diff --git a/.gitattributes b/.gitattributes index 6313b56c578..6dd73a786fb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,9 @@ * text=auto eol=lf +*.gen.ts linguist-generated +*_gen.ts linguist-generated +*_gen.go linguist-generated +**/openapi_snapshots/*.json linguist-generated +apps/**/pkg/apis/*_manifest.go linguist-generated +public/openapi3.json linguist-generated +public/api-merged.json linguist-generated +public/api-enterprise-spec.json linguist-generated diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 70815f59d7b..234789dfa4c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -96,7 +96,9 @@ /apps/iam/ @grafana/access-squad /apps/sdk.mk @grafana/grafana-app-platform-squad /apps/correlations @grafana/datapro +/apps/example/ @grafana/grafana-app-platform-squad /apps/logsdrilldown/ @grafana/observability-logs +/apps/annotation/ @grafana/grafana-backend-services-squad /pkg/api/ @grafana/grafana-backend-group /pkg/apis/ @grafana/grafana-app-platform-squad /pkg/apis/query @grafana/grafana-datasources-core-services @@ -150,7 +152,7 @@ /pkg/promlib @grafana/oss-big-tent /pkg/storage/ @grafana/grafana-search-and-storage /pkg/storage/secret/ @grafana/grafana-operator-experience-squad -/pkg/services/annotations/ @grafana/grafana-search-and-storage +/pkg/services/annotations/ @grafana/grafana-backend-services-squad /pkg/services/apikey/ @grafana/identity-squad /pkg/services/cleanup/ @grafana/grafana-backend-group /pkg/services/contexthandler/ @grafana/grafana-backend-group @grafana/grafana-app-platform-squad @@ -180,7 +182,7 @@ /pkg/services/search/ @grafana/grafana-search-and-storage /pkg/services/searchusers/ @grafana/grafana-search-and-storage /pkg/services/secrets/ @grafana/grafana-operator-experience-squad -/pkg/services/shorturls/ @grafana/grafana-backend-group +/pkg/services/shorturls/ @grafana/sharing-squad /pkg/services/sqlstore/ @grafana/grafana-search-and-storage /pkg/services/ssosettings/ @grafana/identity-squad /pkg/services/star/ @grafana/grafana-search-and-storage @@ -198,6 +200,7 @@ /pkg/tests/apis/features @grafana/grafana-backend-services-squad /pkg/tests/apis/folder @grafana/grafana-search-and-storage /pkg/tests/apis/iam @grafana/identity-access-team +/pkg/tests/apis/shorturl @grafana/sharing-squad /pkg/tests/api/correlations/ @grafana/datapro /pkg/tsdb/grafanads/ @grafana/grafana-backend-group /pkg/tsdb/opentsdb/ @grafana/partner-datasources @@ -224,6 +227,7 @@ /devenv/datasources.yaml @grafana/grafana-backend-group /devenv/datasources_docker.yaml @grafana/grafana-backend-group /devenv/dev-dashboards-without-uid/ @grafana/dashboards-squad +/devenv/scopes/ @grafana/grafana-operator-experience-squad /devenv/dev-dashboards/annotations @grafana/dataviz-squad /devenv/dev-dashboards/migrations @grafana/dataviz-squad @@ -240,6 +244,7 @@ /devenv/dev-dashboards/panel-library @grafana/dataviz-squad /devenv/dev-dashboards/panel-piechart @grafana/dataviz-squad /devenv/dev-dashboards/panel-stat @grafana/dataviz-squad +/devenv/dev-dashboards/panel-status-history @grafana/dataviz-squad /devenv/dev-dashboards/panel-table @grafana/dataviz-squad /devenv/dev-dashboards/panel-timeline @grafana/dataviz-squad /devenv/dev-dashboards/panel-timeseries @grafana/dataviz-squad @@ -249,7 +254,6 @@ /devenv/dev-dashboards/all-panels.json @grafana/dataviz-squad /devenv/dev-dashboards/dashboards.go @grafana/dataviz-squad /devenv/dev-dashboards/home.json @grafana/dataviz-squad - /devenv/dev-dashboards/datasource-elasticsearch/ @grafana/partner-datasources /devenv/dev-dashboards/datasource-opentsdb/ @grafana/partner-datasources /devenv/dev-dashboards/datasource-influxdb/ @grafana/partner-datasources @@ -413,8 +417,8 @@ /crowdin.yml @grafana/grafana-frontend-platform /public/locales/ @grafanabot -/public/locales/i18next-parser.config.cjs @grafana/grafana-frontend-platform -/public/locales/i18next-parser-enterprise.config.cjs @grafana/grafana-frontend-platform +i18next.config.ts @grafana/grafana-frontend-platform +/public/locales/enterprise/i18next.config.ts @grafana/grafana-frontend-platform /public/app/core/internationalization/ @grafana/grafana-frontend-platform /e2e/ @grafana/grafana-frontend-platform /e2e-playwright/cloud-plugins-suite/ @grafana/partner-datasources @@ -471,23 +475,12 @@ /e2e-playwright/fixtures/long-trace-response.json @grafana/observability-traces-and-profiling /e2e-playwright/fixtures/tempo-response.json @grafana/oss-big-tent /e2e-playwright/fixtures/prometheus-response.json @grafana/datapro -/e2e-playwright/panels-suite/canvas-scene.spec.ts @grafana/dataviz-squad +/e2e-playwright/panels-suite/ @grafana/dataviz-squad /e2e-playwright/panels-suite/dashlist.spec.ts @grafana/grafana-search-navigate-organise -/e2e-playwright/panels-suite/datagrid-data-change.spec.ts @grafana/dataviz-squad -/e2e-playwright/panels-suite/datagrid-editing-features.spec.ts @grafana/dataviz-squad /e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts @grafana/plugins-platform-frontend -/e2e-playwright/panels-suite/geomap-layer-types.spec.ts @grafana/dataviz-squad -/e2e-playwright/panels-suite/geomap-map-controls.spec.ts @grafana/dataviz-squad -/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts @grafana/dataviz-squad /e2e-playwright/panels-suite/panelEdit_base.spec.ts @grafana/dashboards-squad /e2e-playwright/panels-suite/panelEdit_queries.spec.ts @grafana/dashboards-squad /e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @grafana/datapro -/e2e-playwright/panels-suite/state-timeline.spec.ts @grafana/dataviz-squad -/e2e-playwright/panels-suite/table-footer.spec.ts @grafana/dataviz-squad -/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @grafana/dataviz-squad -/e2e-playwright/panels-suite/table-markdown.spec.ts @grafana/dataviz-squad -/e2e-playwright/panels-suite/table-sparkline.spec.ts @grafana/dataviz-squad -/e2e-playwright/panels-suite/table-utils.ts @grafana/dataviz-squad /e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources /e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend /e2e-playwright/smoke-tests-suite/ @grafana/grafana-frontend-platform @@ -504,6 +497,7 @@ /e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @grafana/plugins-platform-frontend /e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts @grafana/plugins-platform-frontend /e2e-playwright/various-suite/gauge.spec.ts @grafana/dataviz-squad +/e2e-playwright/various-suite/grafana-datasource-random-walk.spec.ts @grafana/grafana-frontend-platform /e2e-playwright/various-suite/graph-auto-migrate.spec.ts @grafana/dataviz-squad /e2e-playwright/various-suite/inspect-drawer.spec.ts @grafana/dashboards-squad /e2e-playwright/various-suite/keybinds.spec.ts @grafana/grafana-frontend-platform @@ -555,6 +549,7 @@ /packages/grafana-data/src/geo/ @grafana/dataviz-squad /packages/grafana-data/src/monaco/ @grafana/partner-datasources /packages/grafana-data/src/panel/ @grafana/dashboards-squad +/packages/grafana-data/src/panel/suggestions/ @grafana/dataviz-squad /packages/grafana-data/src/query/ @grafana/grafana-datasources-core-services /packages/grafana-data/src/rbac/ @grafana/access-squad /packages/grafana-data/src/table/ @grafana/dataviz-squad @@ -720,6 +715,7 @@ /packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad +/packages/grafana-ui/src/components/RadialGauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/PluginSignatureBadge/ @grafana/plugins-platform-frontend /packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations /packages/grafana-ui/src/components/Table/ @grafana/dataviz-squad @@ -795,7 +791,7 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform /public/app/core/components/ColorScale/ @grafana/dataviz-squad /public/app/core/components/DynamicImports/ @grafana/grafana-search-navigate-organise /public/app/core/components/EmptyListCTA/ @grafana/grafana-frontend-platform -/public/app/core/components/FolderFilter/ @grafana/sharing-squad +/public/app/core/components/FolderFilter/ @grafana/grafana-search-navigate-organise /public/app/core/components/Footer/ @grafana/grafana-search-navigate-organise /public/app/core/components/ForgottenPassword/ @grafana/grafana-search-navigate-organise /public/app/core/components/Form/ @grafana/grafana-frontend-platform @@ -1172,6 +1168,7 @@ embed.go @grafana/grafana-as-code /pkg/registry/ @grafana/grafana-as-code /pkg/registry/apis/ @grafana/grafana-app-platform-squad /pkg/registry/apis/folders @grafana/grafana-search-and-storage +/pkg/registry/apis/datasource @grafana/grafana-datasources-core-services /pkg/registry/apis/query @grafana/grafana-datasources-core-services /pkg/registry/apis/secret @grafana/grafana-operator-experience-squad /pkg/registry/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend diff --git a/.github/actions/change-detection/action.yml b/.github/actions/change-detection/action.yml index 49c1f44477b..2c6b46606f4 100644 --- a/.github/actions/change-detection/action.yml +++ b/.github/actions/change-detection/action.yml @@ -31,6 +31,9 @@ outputs: dockerfile: description: Whether the dockerfile or self have changed in any way value: ${{ steps.changed-files.outputs.dockerfile_any_changed || 'true' }} + devenv: + description: Whether the devenv or self have changed in any way + value: ${{ steps.changed-files.outputs.devenv_any_changed || 'true' }} runs: using: composite steps: @@ -136,6 +139,9 @@ runs: - '.vale.ini' - '.github/actions/change-detection/**' - '${{ inputs.self }}' + devenv: + - 'devenv/**' + - '${{ inputs.self }}' - name: Print all change groups shell: bash run: | @@ -157,3 +163,5 @@ runs: echo " --> ${{ steps.changed-files.outputs.docs_all_changed_files }}" echo "Dockerfile: ${{ steps.changed-files.outputs.dockerfile_any_changed || 'true' }}" echo " --> ${{ steps.changed-files.outputs.dockerfile_all_changed_files }}" + echo "devenv: ${{ steps.changed-files.outputs.devenv_any_changed || 'true' }}" + echo " --> ${{ steps.changed-files.outputs.devenv_all_changed_files }}" diff --git a/.github/renovate.json5 b/.github/renovate.json5 index aa33961bf95..4eb9f7b8ddf 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,6 +1,6 @@ { extends: ["config:recommended"], - enabledManagers: ["npm"], + enabledManagers: ["npm", "docker-compose"], ignorePresets: [ "github>grafana/grafana-renovate-config//presets/labels", ], @@ -26,7 +26,7 @@ "@types/slate-react", // we don't want to continue using this on the long run, use Monaco editor instead of Slate "@types/slate", // we don't want to continue using this on the long run, use Monaco editor instead of Slate ], - includePaths: ["package.json", "packages/**", "public/app/plugins/**"], + includePaths: ["package.json", "packages/**", "public/app/plugins/**", "devenv/frontend-service/docker-compose.yaml"], ignorePaths: ["emails/**", "**/mocks/**"], labels: ["area/frontend", "dependencies", "no-changelog"], postUpdateOptions: ["yarnDedupeHighest"], diff --git a/.github/workflows/backend-code-checks.yml b/.github/workflows/backend-code-checks.yml index 0294a121551..0367ee34641 100644 --- a/.github/workflows/backend-code-checks.yml +++ b/.github/workflows/backend-code-checks.yml @@ -79,7 +79,7 @@ jobs: make swagger-clean && make openapi3-gen # Check if the generated specs differ from what's in the repository - for f in public/api-merged.json public/openapi3.json; do git add $f; done + for f in public/api-merged.json public/openapi3.json public/api-enterprise-spec.json; do git add $f; done if [ -z "$(git diff --name-only --cached)" ]; then echo "OpenAPI specs are up to date!" else diff --git a/.github/workflows/backport-trigger.yml b/.github/workflows/backport-trigger.yml index 3b3758c573b..b6263775da3 100644 --- a/.github/workflows/backport-trigger.yml +++ b/.github/workflows/backport-trigger.yml @@ -40,7 +40,7 @@ jobs: }' "$GITHUB_EVENT_PATH" > /tmp/pr_info.json - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: pr_info path: /tmp/pr_info.json diff --git a/.github/workflows/backport-workflow.yml b/.github/workflows/backport-workflow.yml index f3d6761440c..ef7254ffe6a 100644 --- a/.github/workflows/backport-workflow.yml +++ b/.github/workflows/backport-workflow.yml @@ -36,7 +36,7 @@ jobs: private_key: ${{ fromJSON(steps.secrets.outputs.secrets).APP_PEM }} - name: Download PR info artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 id: download-pr-info with: github-token: ${{ github.token }} diff --git a/.github/workflows/core-plugins-build-and-release.yml b/.github/workflows/core-plugins-build-and-release.yml index de8835420a3..5a4133442a7 100644 --- a/.github/workflows/core-plugins-build-and-release.yml +++ b/.github/workflows/core-plugins-build-and-release.yml @@ -193,7 +193,7 @@ jobs: exit 1 fi - name: store build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: build-artifacts path: ${{ steps.get_dir.outputs.dir }}/ci/packages/*.zip diff --git a/.github/workflows/detect-breaking-changes-levitate.yml b/.github/workflows/detect-breaking-changes-levitate.yml index 2e9b370d4e0..eff55077d7c 100644 --- a/.github/workflows/detect-breaking-changes-levitate.yml +++ b/.github/workflows/detect-breaking-changes-levitate.yml @@ -64,7 +64,7 @@ jobs: run: zip -r ./pr_built_packages.zip ./packages/**/*.tgz - name: Upload build output as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: buildPr path: './pr/pr_built_packages.zip' @@ -116,7 +116,7 @@ jobs: run: zip -r ./base_built_packages.zip ./packages/**/*.tgz - name: Upload build output as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: buildBase path: './base/base_built_packages.zip' @@ -141,12 +141,12 @@ jobs: node-version-file: '.nvmrc' - name: Get built packages from pr - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: buildPr - name: Get built packages from base - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: buildBase @@ -189,7 +189,7 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} - name: Upload check output as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: levitate path: levitate/ @@ -225,7 +225,7 @@ jobs: persist-credentials: false - name: 'Download artifact' - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: levitate diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index 08b5ad7aba9..cf69dde2c93 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -94,14 +94,14 @@ jobs: id: artifact - name: Upload grafana.tar.gz - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: retention-days: 1 name: grafana-tar-gz path: build-dir/grafana.tar.gz - name: Upload grafana docker tarball - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: retention-days: 1 name: grafana-docker-tar-gz @@ -133,7 +133,7 @@ jobs: # We want a static binary, so we need to set CGO_ENABLED=0 CGO_ENABLED=0 go build -o ./e2e-runner ./e2e/ echo "artifact=e2e-runner-${{github.run_number}}" >> "$GITHUB_OUTPUT" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 id: upload with: retention-days: 1 @@ -159,7 +159,7 @@ jobs: with: registry: 'us-docker.pkg.dev' environment: 'dev' - - uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: name: grafana-docker-tar-gz path: . @@ -221,10 +221,10 @@ jobs: - uses: actions/checkout@v5 with: persist-credentials: false - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: grafana-tar-gz - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: ${{ needs.build-e2e-runner.outputs.artifact }} - name: chmod +x @@ -245,7 +245,7 @@ jobs: run: | set -euo pipefail echo "suite=$(echo "$SUITE" | sed 's/\//-/g')" >> "$GITHUB_OUTPUT" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 if: success() || failure() with: name: ${{ steps.set-suite-name.outputs.suite }}-${{ github.run_number }} @@ -298,7 +298,7 @@ jobs: - uses: actions/checkout@v5 with: persist-credentials: false - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: grafana-tar-gz - name: Run E2E tests @@ -307,7 +307,7 @@ jobs: version: 0.18.8 verb: run args: go run ./pkg/build/e2e-playwright --package=grafana.tar.gz --shard=${{ matrix.shard }}/${{ matrix.shardTotal }} --blob-dir=./blob-report - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 if: success() || failure() with: name: playwright-blob-${{ github.run_number }}-${{ matrix.shard }} @@ -360,7 +360,7 @@ jobs: run: | docker cp cpp-e2e-deploy:/outputs.json /tmp/outputs.json - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: grafana-tar-gz @@ -400,7 +400,7 @@ jobs: node-version-file: '.nvmrc' - name: Download blob reports from GitHub Actions Artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: path: blobs pattern: playwright-blob-* @@ -439,7 +439,7 @@ jobs: - name: Upload HTML report id: upload-html - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: playwright-html-${{ github.run_number }} path: playwright-report @@ -479,7 +479,7 @@ jobs: - uses: actions/checkout@v5 with: persist-credentials: false - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: grafana-tar-gz - name: Run PR a11y test @@ -498,7 +498,7 @@ jobs: args: go run ./pkg/build/a11y --package=grafana.tar.gz --no-threshold-fail --results=./pa11y-ci-results.json - name: Upload pa11y results if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: retention-days: 1 name: pa11y-ci-results @@ -531,7 +531,7 @@ jobs: - name: Install dependencies run: yarn install --immutable - name: Get pa11y results - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: pa11y-ci-results - name: Extract and publish metrics diff --git a/.github/workflows/pr-frontend-unit-tests.yml b/.github/workflows/pr-frontend-unit-tests.yml index 9c6c0b32daa..1e4fa7790a6 100644 --- a/.github/workflows/pr-frontend-unit-tests.yml +++ b/.github/workflows/pr-frontend-unit-tests.yml @@ -18,6 +18,7 @@ jobs: contents: read outputs: changed: ${{ steps.detect-changes.outputs.frontend }} + devenv-changed: ${{ steps.detect-changes.outputs.devenv }} steps: - uses: actions/checkout@v5 with: @@ -169,3 +170,26 @@ jobs: needs: ${{ toJson(needs) }} failure-message: "One or more unit test jobs have failed" success-message: "All unit tests completed successfully" + + devenv: + needs: + - detect-changes + if: needs.detect-changes.outputs.devenv-changed == 'true' + runs-on: ubuntu-x64-large + name: "Devenv frontend-service build" + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Setup Docker + uses: docker/setup-docker-action@3fb92d6d9c634363128c8cce4bc3b2826526370a # v4 + - name: Setup Node.js + uses: ./.github/actions/setup-node + - name: Install Tilt + run: curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash + - name: Create empty config files # TODO: the tiltfile should conditionally mount these only if they exist, like the enterprise license + run: | + touch devenv/frontend-service/configs/grafana-api.local.ini + touch devenv/frontend-service/configs/frontend-service.local.ini + - name: Test frontend-service Tiltfile + run: tilt ci --file devenv/frontend-service/Tiltfile diff --git a/.github/workflows/pr-test-integration.yml b/.github/workflows/pr-test-integration.yml index 7e7545bead0..b80d801b1b0 100644 --- a/.github/workflows/pr-test-integration.yml +++ b/.github/workflows/pr-test-integration.yml @@ -78,6 +78,7 @@ jobs: # We don't need more than this since it has to wait for the other tests. shard: [ 1/4, 2/4, 3/4, 4/4, + profiled, ] fail-fast: false @@ -96,13 +97,74 @@ jobs: go-version-file: go.mod cache: true - name: Run tests + if: matrix.shard != 'profiled' env: SHARD: ${{ matrix.shard }} + CGO_ENABLED: 0 + SKIP_PACKAGES: |- + pkg/tests/apis/folder + pkg/tests/apis/dashboard run: | set -euo pipefail - readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/pkgs-with-tests-named.sh -b TestIntegration | ./scripts/ci/backend-tests/shard.sh -N"$SHARD" -d-)" - # ionice since tests are IO intensive - CGO_ENABLED=0 ionice -c2 -n7 go test -p=4 -tags=sqlite -timeout=8m -run '^TestIntegration' "${PACKAGES[@]}" + # Build regex pattern like: pkg1$|pkg2$|pkg3$ + SKIP_PATTERN=$(echo "$SKIP_PACKAGES" | sed '/^$/d' | sed 's|.*|&$|' | paste -sd '|' -) + readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/pkgs-with-tests-named.sh -b TestIntegration | ./scripts/ci/backend-tests/shard.sh -N "$SHARD" -d - | grep -Ev "($SKIP_PATTERN)")" + go test -tags=sqlite -timeout=8m -run '^TestIntegration' "${PACKAGES[@]}" + - name: Run profiled tests + id: run-profiled-tests + if: matrix.shard == 'profiled' + env: + CGO_ENABLED: 0 + PROFILED_PACKAGES: |- + pkg/tests/apis/folder + pkg/tests/apis/dashboard + run: | + set -euo pipefail + # Build regex pattern line: pkg1$|pkg2$|pkg3$ + PROFILE_PATTERN=$(echo "$PROFILED_PACKAGES" | sed '/^$/d' | sed 's|.*|&$|' | paste -sd '|' -) + readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/pkgs-with-tests-named.sh -b TestIntegration | grep -E "($PROFILE_PATTERN)")" + if [ ${#PACKAGES[@]} -eq 0 ]; then + echo "⚠️ No profiled packages found" + exit 0 + fi + mkdir -p profiles + EXIT_CODE=0 + # Run each profiled package sequentially + for full_pkg in "${PACKAGES[@]}"; do + # Build valid file name + pkg_name=$(basename "$full_pkg" | tr '/' '_' | tr '.' '_') + echo "📦 Running $full_pkg" + set +e + go test -tags=sqlite -timeout=8m -run '^TestIntegration' \ + -outputdir=profiles \ + -cpuprofile="cpu_${pkg_name}.prof" \ + -memprofile="mem_${pkg_name}.prof" \ + -trace="trace_${pkg_name}.out" \ + "$full_pkg" 2>&1 | tee "profiles/test_${pkg_name}.log" + TEST_EXIT=$? + set -e + if [ $TEST_EXIT -ne 0 ]; then + echo "❌ $full_pkg failed with exit code $TEST_EXIT" + EXIT_CODE=1 + else + echo "✅ $full_pkg passed" + fi + done + # Set output for artifact upload + if [ $EXIT_CODE -ne 0 ]; then + echo "upload_artifacts=true" >> "$GITHUB_OUTPUT" + else + echo "upload_artifacts=false" >> "$GITHUB_OUTPUT" + fi + exit $EXIT_CODE + - name: Output test profiles and traces + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4 + if: matrix.shard == 'profiled' && !cancelled() && steps.run-profiled-tests.outputs.upload_artifacts == 'true' + with: + name: integration-test-profiles-sqlite-nocgo-${{ github.run_number }} + path: profiles/ + retention-days: 7 + if-no-files-found: ignore mysql: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' diff --git a/.github/workflows/publish-artifact.yml b/.github/workflows/publish-artifact.yml index 8ea0b9b90a0..ad0cc86de66 100644 --- a/.github/workflows/publish-artifact.yml +++ b/.github/workflows/publish-artifact.yml @@ -44,7 +44,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: name: ${{ inputs.name }} pattern: ${{ inputs.pattern }} diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 2fc23359a7a..9430cb083ab 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -187,12 +187,12 @@ jobs: output: artifacts-${{ matrix.name }}.txt verify: ${{ matrix.verify }} build-id: ${{ github.run_id }} - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: artifacts-list-${{ matrix.name }} path: ${{ steps.build.outputs.file }} retention-days: 1 - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: artifacts-${{ matrix.name }} path: ${{ steps.build.outputs.dist-dir }} @@ -224,27 +224,27 @@ jobs: - build steps: - uses: grafana/shared-workflows/actions/dockerhub-login@dockerhub-login/v1.0.2 - - uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: name: artifacts-list-linux-amd64 path: . - - uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: name: artifacts-list-linux-arm64 path: . - - uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: name: artifacts-list-linux-armv7 path: . - - uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: name: artifacts-linux-amd64 path: dist - - uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: name: artifacts-linux-arm64 path: dist - - uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 with: name: artifacts-linux-armv7 path: dist diff --git a/.github/workflows/storybook-a11y.yml b/.github/workflows/storybook-a11y.yml index 5736c86d8bf..04d01151bae 100644 --- a/.github/workflows/storybook-a11y.yml +++ b/.github/workflows/storybook-a11y.yml @@ -34,7 +34,7 @@ jobs: id-token: write needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' - name: "Run Storybook a11y tests" + name: "Run Storybook a11y tests (light theme)" steps: - uses: actions/checkout@v5 with: @@ -64,7 +64,7 @@ jobs: id-token: write needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' - name: "Run Storybook a11y tests" + name: "Run Storybook a11y tests (dark theme)" steps: - uses: actions/checkout@v5 with: diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index 7278c8ad3ed..c5bc7fd095e 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -31,6 +31,6 @@ jobs: persist-credentials: false fetch-depth: ${{ steps.fetch_depth.outputs.fetch_depth }} - name: Trufflehog - uses: trufflesecurity/trufflehog@ad6fc8fb446b8fafbf7ea8193d2d6bfd42f45690 # v3.90.11 + uses: trufflesecurity/trufflehog@b84c3d14d189e16da175e2c27fa8136603783ffc # v3.90.12 with: extra_args: --results=verified diff --git a/.gitignore b/.gitignore index 036bb5dd2b1..d0e233d6a42 100644 --- a/.gitignore +++ b/.gitignore @@ -250,9 +250,12 @@ public/mockServiceWorker.js /e2e-playwright/test-plugins/*/dist /apps/provisioning/cmd/job-controller/bin/ - # Ignore unified storage kv store files /grafana-kv-data + +# Ignore debug output from test library +/pkg/storage/secret/metadata/testdata/rapid/TestStateMachine/ + /codeowners-manifest/ # Ignore grafana/hippocampus local cache folder diff --git a/.golangci.yml b/.golangci.yml index 1fee8547139..33b2b9e8a19 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -105,6 +105,8 @@ linters: - '**/pkg/tsdb/graphite/**/*' - '**/pkg/tsdb/mysql/*' - '**/pkg/tsdb/mysql/**/*' + - '**/pkg/tsdb/opentsdb/*' + - '**/pkg/tsdb/opentsdb/**/*' - '**/pkg/tsdb/parca/*' - '**/pkg/tsdb/parca/**/*' - '**/pkg/tsdb/tempo/*' diff --git a/.nvmrc b/.nvmrc index aebd91c521b..c519bf5baaa 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.16.0 +v24.11.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1f5d9ddace..c62e18c754e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,9 @@ Thank you for your interest in contributing to Grafana! We welcome all people who want to contribute in a healthy and constructive manner within our community. To help us create a safe and positive community experience for all, we require all participants to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md). -This document is a guide to help you through the process of contributing to Grafana. Be sure to check out the [Grafana Champions program](https://grafana.com/community/champions/?src=github&camp=community-cross-platform-engagement) as you start to contribute. It’s designed to recognize and empower individuals who are actively contributing to the growth and success of the Grafana ecosystem. +This document is a guide to help you through the process of contributing to Grafana. Be sure to check out the [Grafana Champions program](https://grafana.com/community/champions/?src=github&camp=community-cross-platform-engagement) as you start to contribute. It's designed to recognize and empower individuals who are actively contributing to the growth and success of the Grafana ecosystem. + +> **Help us improve!** We'd love to hear about your contributor experience. Take a moment to share your feedback in our [Open Source Contributor Experience Survey](https://gra.fan/ome). Your input helps us make contributing to Grafana better for everyone. Whether you're a new contributor or a seasoned veteran, we hope these resources help you connect with the community. diff --git a/Dockerfile b/Dockerfile index 16efe807bfc..7cb65f4a05f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ARG JS_SRC=js-builder FROM alpine:3.22.2 AS alpine-base FROM ubuntu:22.04 AS ubuntu-base FROM golang:1.25.3-alpine AS go-builder-base -FROM --platform=${JS_PLATFORM} node:22-alpine AS js-builder-base +FROM --platform=${JS_PLATFORM} node:24-alpine AS js-builder-base # Javascript build stage FROM --platform=${JS_PLATFORM} ${JS_IMAGE} AS js-builder ARG JS_NODE_ENV=production @@ -95,6 +95,7 @@ COPY pkg/aggregator pkg/aggregator COPY apps/playlist apps/playlist COPY apps/plugins apps/plugins COPY apps/shorturl apps/shorturl +COPY apps/annotation apps/annotation COPY apps/correlations apps/correlations COPY apps/preferences apps/preferences COPY apps/provisioning apps/provisioning @@ -114,6 +115,7 @@ COPY apps/alerting/notifications apps/alerting/notifications COPY apps/alerting/rules apps/alerting/rules COPY pkg/codegen pkg/codegen COPY pkg/plugins/codegen pkg/plugins/codegen +COPY apps/example apps/example RUN go mod download diff --git a/Makefile b/Makefile index c2ac23809c5..56c2a2c63ff 100644 --- a/Makefile +++ b/Makefile @@ -135,14 +135,14 @@ i18n-extract-enterprise: @echo "Skipping i18n extract for Enterprise: not enabled" else i18n-extract-enterprise: - @echo "Extracting i18n strings for Enterprise" - yarn run i18next --config public/locales/i18next-parser-enterprise.config.cjs + @echo "Extracting i18n strings for Enterprise" + cd public/locales/enterprise && yarn run i18next-cli extract --sync-primary endif .PHONY: i18n-extract i18n-extract: i18n-extract-enterprise @echo "Extracting i18n strings for OSS" - yarn run i18next --config public/locales/i18next-parser.config.cjs + yarn run i18next-cli extract --sync-primary @echo "Extracting i18n strings for packages" yarn run packages:i18n-extract @echo "Extracting i18n strings for plugins" @@ -178,7 +178,7 @@ gen-apps: do-gen-apps gofmt ## Generate code for Grafana App SDK apps and run go @if [ -n "$$CODEGEN_VERIFY" ]; then \ echo "Verifying generated code is up to date..."; \ if ! git diff --quiet; then \ - echo "Error: Generated apps code is not up to date. Please run 'make gen-apps' to regenerate."; \ + echo "Error: Generated code is not up to date. Please run 'make gen-apps', 'make gen-cue', and 'make gen-jsonnet' to regenerate."; \ git diff --name-only; \ exit 1; \ fi; \ diff --git a/README.md b/README.md index 033ebdc1ea6..d92844ed9aa 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ The open-source platform for monitoring and observability [![License](https://img.shields.io/github/license/grafana/grafana)](LICENSE) -[![Drone](https://drone.grafana.net/api/badges/grafana/grafana/status.svg)](https://drone.grafana.net/grafana/grafana) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/grafana)](https://goreportcard.com/report/github.com/grafana/grafana) Grafana allows you to query, visualize, alert on and understand your metrics no matter where they are stored. Create, explore, and share dashboards with your team and foster a data-driven culture: @@ -36,6 +35,8 @@ If you're interested in contributing to the Grafana project: - Explore our [beginner-friendly issues](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22). - Look through our [style guide and Storybook](https://developers.grafana.com/ui/latest/index.html). +> Share your contributor experience in our [feedback survey](https://gra.fan/ome) to help us improve. + ## Get involved - Follow [@grafana on X (formerly Twitter)](https://x.com/grafana/). diff --git a/apps/advisor/Makefile b/apps/advisor/Makefile index aaac06377ca..7ea492797fe 100644 --- a/apps/advisor/Makefile +++ b/apps/advisor/Makefile @@ -1,5 +1,34 @@ include ../sdk.mk -.PHONY: generate +.PHONY: etcd +etcd: + @docker run -d --name etcd --env ALLOW_NONE_AUTHENTICATION=yes -p 22379:2379 bitnamilegacy/etcd:latest + +.PHONY: generate # Run Grafana App SDK code generation generate: install-app-sdk update-app-sdk - @$(APP_SDK_BIN) generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none --useoldmanifestkinds + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --defencoding=none + +.PHONY: run +run: + @go run ./pkg/standalone/server.go --etcd-servers=http://127.0.0.1:22379 --secure-port 7445 + +.PHONY: create-checks +create-checks: + @echo "Creating plugin check..." + @curl -k -X POST https://localhost:7445/apis/advisor.grafana.app/v0alpha1/namespaces/stacks-1/checks \ + -H "Content-Type: application/json" \ + -d '{"kind":"Check","apiVersion":"advisor.grafana.app/v0alpha1","spec":{"data":{}},"metadata":{"generateName":"check-","labels":{"advisor.grafana.app/type":"plugin"},"namespace":"stacks-1"},"status":{"report":{"count":0,"failures":[]}}}' \ + && echo "Plugin check created successfully" + @echo "Creating datasource check..." + @curl -k -X POST https://localhost:7445/apis/advisor.grafana.app/v0alpha1/namespaces/stacks-1/checks \ + -H "Content-Type: application/json" \ + -d '{"kind":"Check","apiVersion":"advisor.grafana.app/v0alpha1","spec":{"data":{}},"metadata":{"generateName":"check-","labels":{"advisor.grafana.app/type":"datasource"},"namespace":"stacks-1"},"status":{"report":{"count":0,"failures":[]}}}' \ + && echo "Datasource check created successfully" + +delete-checks: + @curl -k -X DELETE https://localhost:7445/apis/advisor.grafana.app/v0alpha1/namespaces/stacks-1/checks \ + && echo "All checks deleted successfully" diff --git a/apps/advisor/README.md b/apps/advisor/README.md index c7929d9db62..410a54f89e3 100644 --- a/apps/advisor/README.md +++ b/apps/advisor/README.md @@ -152,3 +152,28 @@ Check [`security_config_step.go`](./pkg/app/checks/configchecks/security_config_ ## Testing Create tests for your check and its steps to ensure they work as expected. Test both successful and failure scenarios. + +## Running the Standalone Mode + +To run the standalone mode, you can use the `make run` command. This will start the advisor app in standalone mode, which means it will not be running in a Kubernetes cluster. + +```bash +make etcd # Start etcd in a docker container +make run # Start the advisor app in standalone mode +``` + +This will start the advisor app on port 7445. You can then access the advisor app at `http://localhost:7445`. + +To see some sample checks, you can run the following command: + +```bash +make create-checks +``` + +Then you can see list in the URL: `http://localhost:7445/apis/advisor.grafana.app/v0alpha1/namespaces/stacks-1/checks` + +To delete all checks, you can run the following command: + +```bash +make delete-checks +``` diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 29fd3c33281..d646f81e055 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -4,55 +4,78 @@ go 1.25.3 require ( github.com/Masterminds/semver/v3 v3.4.0 + github.com/google/go-cmp v0.7.0 github.com/google/go-github/v70 v70.0.0 - github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 + github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.40.2 - github.com/grafana/grafana-app-sdk/logging v0.40.2 - github.com/grafana/grafana-plugin-sdk-go v0.278.0 - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 - github.com/stretchr/testify v1.10.0 - k8s.io/apimachinery v0.33.3 - k8s.io/apiserver v0.33.3 - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-plugin-sdk-go v0.281.0 + github.com/grafana/grafana/pkg/apimachinery v0.0.0 + github.com/stretchr/testify v1.11.1 + k8s.io/apimachinery v0.34.1 + k8s.io/apiserver v0.34.1 + k8s.io/client-go v0.34.1 + k8s.io/component-base v0.34.1 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) // transitive dependencies that need replaced // TODO: stop depending on grafana core replace github.com/grafana/grafana => ../.. -replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250620093340-be61a673dee6 +replace github.com/grafana/grafana/apps/provisioning => ../provisioning + +replace github.com/grafana/grafana/pkg/apimachinery => ../../pkg/apimachinery + +replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver + +replace github.com/grafana/grafana/apps/dashboard => ../dashboard + +replace github.com/grafana/grafana/pkg/aggregator => ../../pkg/aggregator + +replace github.com/grafana/grafana/apps/folder => ../folder + +replace github.com/grafana/grafana/apps/secret => ../secret + +replace github.com/grafana/grafana/apps/iam => ../iam + +replace github.com/grafana/grafana/apps/plugins => ../plugins + +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 require ( + cel.dev/expr v0.24.0 // indirect cloud.google.com/go/compute/metadata v0.7.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/apache/arrow-go/v18 v18.3.0 // indirect + github.com/apache/arrow-go/v18 v18.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/at-wat/mqtt-go v0.19.4 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect - github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect - github.com/aws/smithy-go v1.22.4 // indirect + github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect + github.com/aws/smithy-go v1.23.1 // indirect github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -62,13 +85,12 @@ require ( github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bwmarrin/snowflake v0.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect - github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 // indirect github.com/cloudflare/circl v1.6.1 // indirect + github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect @@ -78,102 +100,110 @@ require ( github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad // indirect github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e // indirect github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect - github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 // indirect + github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/elazarl/goproxy v1.7.2 // indirect - github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gchaincl/sqlhooks v1.3.0 // indirect - github.com/getkin/kin-openapi v0.132.0 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect + github.com/go-openapi/analysis v0.24.0 // indirect + github.com/go-openapi/errors v0.22.3 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/loads v0.23.1 // indirect github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/spec v0.22.0 // indirect + github.com/go-openapi/strfmt v0.24.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-openapi/validate v0.24.0 // indirect - github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-openapi/swag/conv v0.25.1 // indirect + github.com/go-openapi/swag/fileutils v0.25.1 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag/jsonutils v0.25.1 // indirect + github.com/go-openapi/swag/loading v0.25.1 // indirect + github.com/go-openapi/swag/mangling v0.25.1 // indirect + github.com/go-openapi/swag/stringutils v0.25.1 // indirect + github.com/go-openapi/swag/typeutils v0.25.1 // indirect + github.com/go-openapi/swag/yamlutils v0.25.1 // indirect + github.com/go-openapi/validate v0.25.0 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.3 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-migrate/migrate/v4 v4.7.0 // indirect - github.com/golang/mock v1.7.0-rc.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.26.1 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/google/wire v0.6.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263 // indirect - github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 // indirect + github.com/google/wire v0.7.0 // indirect + github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae // indirect + github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect - github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // indirect - github.com/grafana/grafana-aws-sdk v1.1.0 // indirect - github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // indirect - github.com/grafana/grafana/apps/provisioning v0.0.0-20250804150913-990f1c69ecc2 // indirect - github.com/grafana/grafana/pkg/apiserver v0.0.0-20250804150913-990f1c69ecc2 // indirect + github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect + github.com/grafana/grafana-aws-sdk v1.3.0 // indirect + github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect + github.com/grafana/grafana/apps/plugins v0.0.0 // indirect + github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect + github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect - github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect - github.com/grafana/sqlds/v4 v4.2.4 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/grafana/sqlds/v4 v4.2.7 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.4 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/memberlist v0.5.2 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jaegertracing/jaeger-idl v0.5.0 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect github.com/jmespath-community/go-jmespath v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lestrrat-go/strftime v1.0.4 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/vsock v1.2.1 // indirect @@ -188,7 +218,7 @@ require ( github.com/mithrandie/go-text v1.6.0 // indirect github.com/mithrandie/ternary v1.1.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect @@ -199,9 +229,9 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/open-feature/go-sdk v1.14.1 // indirect - github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3 // indirect - github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 // indirect + github.com/open-feature/go-sdk v1.16.0 // indirect + github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 // indirect + github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect @@ -209,41 +239,43 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/alertmanager v0.28.0 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/common v0.67.1 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.14.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/redis/go-redis/v9 v9.14.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect - github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect + github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.7 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tetratelabs/wazero v1.8.2 // indirect github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect - github.com/tjhop/slog-gokit v0.1.3 // indirect - github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect - github.com/unknwon/com v1.0.1 // indirect - github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect - github.com/urfave/cli v1.22.16 // indirect + github.com/tjhop/slog-gokit v0.1.5 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.mongodb.org/mongo-driver v1.17.3 // indirect + go.etcd.io/etcd/api/v3 v3.6.4 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect + go.etcd.io/etcd/client/v3 v3.6.4 // indirect + go.mongodb.org/mongo-driver v1.17.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 // indirect - go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect @@ -252,48 +284,55 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/atomic v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/term v0.33.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.35.0 // indirect + go.uber.org/mock v0.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.74.2 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/mail.v2 v2.3.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect - gopkg.in/telebot.v3 v3.2.1 // indirect + gopkg.in/telebot.v3 v3.3.8 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.3 // indirect - k8s.io/apiextensions-apiserver v0.33.3 // indirect - k8s.io/client-go v0.33.3 // indirect - k8s.io/component-base v0.33.3 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect - modernc.org/libc v1.65.0 // indirect + k8s.io/kms v0.34.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.10.0 // indirect - modernc.org/sqlite v1.38.0 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.39.1 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/yaml v1.5.0 // indirect - xorm.io/builder v0.3.6 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect + xorm.io/builder v0.3.13 // indirect ) diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 982588e1749..8d52f655960 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -32,10 +32,10 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= -cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= -cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= -cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs= +cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s= +cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= +cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -77,15 +77,17 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= @@ -98,20 +100,19 @@ github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= @@ -120,6 +121,8 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= @@ -128,13 +131,14 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= -github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= 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/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgXrwqZOwZ2DAc/aCi3Bu3xENpspW935vxu0= github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f/go.mod h1:f3HiCrHjHBdcm6E83vGaXh1KomZMA2P6aeo3hKx/wg0= +github.com/Yiling-J/theine-go v0.6.2 h1:1GeoXeQ0O0AUkiwj2S9Jc0Mzx+hpqzmqsJ4kIC4M9AY= +github.com/Yiling-J/theine-go v0.6.2/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -142,16 +146,16 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/apache/arrow-go/v18 v18.3.0 h1:Xq4A6dZj9Nu33sqZibzn012LNnewkTUlfKVUFD/RX/I= -github.com/apache/arrow-go/v18 v18.3.0/go.mod h1:eEM1DnUTHhgGAjf/ChvOAQbUQ+EPohtDrArffvUjPg8= +github.com/apache/arrow-go/v18 v18.4.1 h1:q/jVkBWCJOB9reDgaIZIdruLQUb1kbkvOnOFezVH1C4= +github.com/apache/arrow-go/v18 v18.4.1/go.mod h1:tLyFubsAl17bvFdUAy24bsSvA/6ww95Iqi67fTpGu3E= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= -github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= +github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= @@ -168,46 +172,44 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= -github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= +github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= -github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= -github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= -github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= -github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 h1:6VFPH/Zi9xYFMJKPQOX5URYkQoXRWeJ7V/7Y6ZDYoms= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= +github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4= +github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8= +github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4= -github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= -github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= -github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= -github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0= -github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= +github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= +github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= +github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= @@ -264,29 +266,23 @@ github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= -github.com/blugelabs/bluge v0.2.2 h1:gat8CqE6P6tOgeX30XGLOVNTC26cpM2RWVcreXWtYcM= -github.com/blugelabs/bluge v0.2.2/go.mod h1:am1LU9jS8dZgWkRzkGLQN3757EgMs3upWrU2fdN9foE= -github.com/blugelabs/bluge_segment_api v0.2.0 h1:cCX1Y2y8v0LZ7+EEJ6gH7dW6TtVTW4RhG0vp3R+N2Lo= -github.com/blugelabs/bluge_segment_api v0.2.0/go.mod h1:95XA+ZXfRj/IXADm7gZ+iTcWOJPg5jQTY1EReIzl3LA= -github.com/blugelabs/ice v1.0.0 h1:um7wf9e6jbkTVCrOyQq3tKK43fBMOvLUYxbj3Qtc4eo= -github.com/blugelabs/ice v1.0.0/go.mod h1:gNfFPk5zM+yxJROhthxhVQYjpBO9amuxWXJQ2Lo+IbQ= -github.com/blugelabs/ice/v2 v2.0.1 h1:mzHbntLjk2v7eDRgoXCgzOsPKN1Tenu9Svo6l9cTLS4= -github.com/blugelabs/ice/v2 v2.0.1/go.mod h1:QxAWSPNwZwsIqS25c3lbIPFQrVvT1sphf5x5DfMLH5M= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= -github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= -github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= -github.com/caio/go-tdigest v3.1.0+incompatible h1:uoVMJ3Q5lXmVLCCqaMGHLBWnbGoN6Lpu7OAUPR60cds= -github.com/caio/go-tdigest v3.1.0+incompatible/go.mod h1:sHQM/ubZStBUmF1WbB8FAm8q9GjDajLC5T7ydxE3JHI= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -294,8 +290,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= -github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 h1:UZdrvid2JFwnvPlUSEFlE794XZL4Jmrj8fuxfcLECJE= -github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -327,9 +321,6 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= @@ -353,8 +344,6 @@ github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9j github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= -github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= -github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= @@ -377,8 +366,8 @@ github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e h1:7pAt github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e/go.mod h1:KZyoO3jngyZCLyCf100FEQTrwAHj33AIMj4Zv4u3MNE= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= -github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 h1:LGTt2LtYX8vaai32d+c9L0sMcP+Dg9w1kO6+lbsxxYg= -github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33 h1:ScHTwNbcVC6JH1OSyXzj8S4w67BIpRXwTSjrac3/PSw= +github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33/go.mod h1:8pvvk5OLaLN9LLxghyczUapn/97l+mBgIb10qC1LG84= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -386,10 +375,10 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= -github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -405,8 +394,8 @@ github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= @@ -425,22 +414,20 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk= github.com/fullstorydev/grpchan v1.1.1 h1:heQqIJlAv5Cnks9a70GRL2EJke6QQoUB25VGR6TZQas= github.com/fullstorydev/grpchan v1.1.1/go.mod h1:f4HpiV8V6htfY/K44GWV1ESQzHBTq7DinhzqQ95lpgc= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gchaincl/sqlhooks v1.3.0 h1:yKPXxW9a5CjXaVf2HkQn6wn7TZARvbAOAelr3H8vK2Y= github.com/gchaincl/sqlhooks v1.3.0/go.mod h1:9BypXnereMT0+Ys8WGWHqzgkkOfHIhyeUCqXC24ra34= -github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= -github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= -github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= -github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= +github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -451,42 +438,62 @@ github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXg github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= +github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA= +github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw= +github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs= +github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0= +github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44= github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw= +github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0= +github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc= +github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= +github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= +github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= +github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= +github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= +github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= +github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= +github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= +github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU= +github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -496,8 +503,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= -github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -518,8 +525,8 @@ github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= -github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= @@ -529,8 +536,6 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -540,8 +545,6 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= -github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -571,12 +574,12 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= -github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -591,7 +594,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -626,12 +628,11 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= -github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= +github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -641,66 +642,54 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263 h1:hcr/AmPB0KL4H+gCEFIdKUnkihTxGAkAOiZA7GDYoL8= -github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls= -github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 h1:vVPT0i5Y1vI6qzecYStV2yk7cHKrC3Pc7AgvwT5KydQ= -github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= -github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/oUn6Cr90QbJYpQJ4FnjyAIG9Ex5GtTZIzw= -github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae h1:NLPwY3tIP0lg0g9wTRiMcypm6VRXW6W+MOLBsq8JSVA= +github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= +github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= +github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= +github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= +github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= -github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE= -github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= -github.com/grafana/grafana-app-sdk v0.40.2 h1:j2ftFuqhX+exYUipfEjeWDs3i7oiJkweTF8gFLL7wWU= -github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= -github.com/grafana/grafana-app-sdk/logging v0.40.2 h1:HQ1+y9Od92iMbWWB54QxiYpNtCvYGUVpyxvxZ7ywB1k= -github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= -github.com/grafana/grafana-aws-sdk v1.1.0 h1:G0fvwbQmHw14c5RXPd7Gnw9ZQcgzl139LtMDoe0KhmE= -github.com/grafana/grafana-aws-sdk v1.1.0/go.mod h1:7e+47EdHynteYWGoT5Ere9KeOXQObsk8F0vkOLQ1tz8= -github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= -github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= -github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= -github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250804150913-990f1c69ecc2 h1:M5kMQVRjcjtnGuw0RM9/iab5VpmJyBwpEDbSfS6IQV4= -github.com/grafana/grafana/apps/dashboard v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:Ik6PLuCNceqTeOesGeQLn4hVL5wCbfscuAEhBEdq8js= -github.com/grafana/grafana/apps/folder v0.0.0-20250804150913-990f1c69ecc2 h1:tubqrwcZaJzS3DM2bLJxK6OCUsmBK0Vs95HiBuhCQcU= -github.com/grafana/grafana/apps/folder v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:la6q+Qvy0JGzBu/yyjC+e7xPefPM7CEfEgD574R2QMU= -github.com/grafana/grafana/apps/provisioning v0.0.0-20250804150913-990f1c69ecc2 h1:83OGBndFNYijNjgQpSf4y1bSNlscVDq8ru97Q6PyU6o= -github.com/grafana/grafana/apps/provisioning v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:qzFUVwLI1b5UIbVFxFydUYAsnOK27AgtA5so3EW8jM0= -github.com/grafana/grafana/apps/secret v0.0.0-20250804150913-990f1c69ecc2 h1:r5oGyvRVxljzKHTQYHtpmK2Et+BVhWQVw7aXo+FUygk= -github.com/grafana/grafana/apps/secret v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:pS2M5ILsHx9VNTM96glLtCjCVXHWyfGcT34WHvbbMtM= -github.com/grafana/grafana/pkg/aggregator v0.0.0-20250804150913-990f1c69ecc2 h1:1AdHE94bYfEG7cuFb92wFlBaVPeexF21l9zCFmN8im0= -github.com/grafana/grafana/pkg/aggregator v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:wU4CoDIHuOOL7ZnchyvfER5UAcEz94A+ZkY+Xt+Ks/8= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= -github.com/grafana/grafana/pkg/apiserver v0.0.0-20250804150913-990f1c69ecc2 h1:oUINBBiA25EwjOFXJVa4rmU8jHlOGn5EdPCmqpIqy4Q= -github.com/grafana/grafana/pkg/apiserver v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:BA9Rvm6co9tbrZ0lS8ScqLFvAnDQ4lZOyplJgB+yMQQ= +github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= +github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= +github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= +github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= +github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc= +github.com/grafana/grafana-plugin-sdk-go v0.281.0 h1:V8dGyatzcOLQeivFhBV2JWMwTSZH/clDnpfKG9p3dTA= +github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ= 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.20250620093340-be61a673dee6 h1:oJnbhG6ZNy10AjsgNeAtAKeGHogIGOMfAsBH6fYYa5M= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20250620093340-be61a673dee6/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= -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/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= -github.com/grafana/sqlds/v4 v4.2.4 h1:Xlxy1udWqDK0dlbuJ1qXL7K3EYaf+aKMl38zhd3VbQY= -github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= +github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA= +github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= @@ -709,8 +698,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/api v1.31.2 h1:NicObVJHcCmyOIl7Z9iHPvvFrocgTYo9cITSGg0/7pw= @@ -739,8 +728,8 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= -github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= @@ -769,8 +758,8 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -781,13 +770,22 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= -github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= -github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4= github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -795,8 +793,10 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -812,9 +812,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ= github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o= -github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= @@ -827,8 +824,8 @@ github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -846,6 +843,10 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= @@ -855,8 +856,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= -github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/madflojo/testcerts v1.4.0 h1:I09gN0C1ly9IgeVNcAqKk8RAKIJTe3QnFrrPBDyvzN4= +github.com/madflojo/testcerts v1.4.0/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLDUsQhpmkuo0= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= @@ -885,8 +886,9 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -894,6 +896,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= @@ -926,15 +930,16 @@ github.com/mithrandie/go-text v1.6.0 h1:8gOXTMPbMY8DJbKMTv8kHhADcJlDWXqS/YQH4SyW github.com/mithrandie/go-text v1.6.0/go.mod h1:xCgj1xiNbI/d4xA9sLVvXkjh5B2tNx2ZT2/3rpmh8to= github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QRdC4= github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g= -github.com/mocktools/go-smtp-mock/v2 v2.3.1 h1:wq75NDSsOy5oHo/gEQQT0fRRaYKRqr1IdkjhIPXxagM= -github.com/mocktools/go-smtp-mock/v2 v2.3.1/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M= +github.com/mocktools/go-smtp-mock/v2 v2.5.1 h1:QcMJMChSgG1olVj4o6xxQFdrWzRjYNrcq660HAjd0wA= +github.com/mocktools/go-smtp-mock/v2 v2.5.1/go.mod h1:Rr8M2njlxx//l5INl2+uESnsL2lDsL24teEykCrGfmE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= @@ -946,12 +951,12 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk= +github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw= github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= @@ -960,25 +965,32 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= -github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= -github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3 h1:6jpO63NCEZv4xunJj+aNlDuFVuRkVBPMcIuxvFPYRWQ= -github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3/go.mod h1:dPUHjAIFzg+ci/wt6XxlNiiMkOh5Yw4SGyeRY0AFT0g= -github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 h1:ZdqlGnNwhWf3luhBQlIpbglvcCzjkcuEgOEhYhr5Emc= -github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE= +github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg= +github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo= +github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 h1:megzzlQGjsRVWDX8oJnLaa5eEcsAHekiL4Uvl3jSAcY= +github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6/go.mod h1:K1gDKvt76CGFLSUMHUydd5ba2V5Cv69gQZsdbnXhAm8= +github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 h1:WinefYxeVx5rV0uQmuWbxQf8iACu/JiRubo5w0saToc= +github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6/go.mod h1:Dwcaoma6lZVqYwyfVlY7eB6RXbG+Ju3b9cnpTlUN+Hc= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 h1:58mhO5nqkdka2Mpg5mijuZOHScX7reowhzRciwjFCU8= +github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67/go.mod h1:XDX4qYNBUM2Rsa2AbKPh+oocZc2zgme+EF2fFC6amVU= +github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe h1:X1g0rBUMvvzMudsak/jmoEZ1NhSsp6yR0VGxWHnGMzs= +github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe/go.mod h1:5Z0pbTT7Jz/oQFLfadb+C5t5NwHrduAO7j7L07Ec1GM= +github.com/openfga/openfga v1.10.0 h1:Ieq4fjJeT3KrxRiekgCnp9VCoXXnBvYITcTR35SPlYY= +github.com/openfga/openfga v1.10.0/go.mod h1:6/m4GTwQsqECsGYQVD3t5sCX97rh3smnmxbMa3YAtJk= github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w= github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -993,8 +1005,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -1014,7 +1026,10 @@ 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng= +github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= @@ -1022,8 +1037,8 @@ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -1032,6 +1047,7 @@ github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= @@ -1039,13 +1055,14 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= @@ -1058,8 +1075,8 @@ github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0n github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= -github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= +github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -1072,48 +1089,54 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU= -github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 h1:OfRzdxCzDhp+rsKWXuOO2I/quKMJ/+TQwVbIP/gltZg= +github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92/go.mod h1:7/OT02F6S6I7v6WXb+IjhMuZEYfH/RJ5RwEWnEo5BMg= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -1130,11 +1153,14 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= @@ -1142,8 +1168,10 @@ github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574 github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE9IrvgBA2C1mRN1lc= github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o= -github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= +github.com/tjhop/slog-gokit v0.1.5 h1:ayloIUi5EK2QYB8eY4DOPO95/mRtMW42lUkp3quJohc= +github.com/tjhop/slog-gokit v0.1.5/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= @@ -1151,31 +1179,23 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= -github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4= -github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= -github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= -github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= -github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a h1:vcrhXnj9g9PIE+cmZgaPSwOyJ8MAQTRmsgGrB0x5rF4= -github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= -github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= @@ -1183,21 +1203,27 @@ github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtC github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= -go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= +go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= -go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= +go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= +go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= -go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= +go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= +go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= -go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= +go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= +go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= +go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA= +go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE= +go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= +go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= +go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= -go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -1206,88 +1232,88 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= +go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 h1:lREC4C0ilyP4WibDhQ7Gg2ygAQFP8oR07Fst/5cafwI= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0/go.mod h1:HfvuU0kW9HewH14VCOLImqKvUgONodURG7Alj/IrnGI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 h1:SoCgXYF4ISDtNyfLUzsGDaaudZVTx2yJhOyBO0+/GYk= -go.opentelemetry.io/contrib/propagators/jaeger v1.36.0/go.mod h1:VHu48l0YTRKSObdPQ+Sb8xMZvdnJlN7yhHuHoPgNqHM= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0 h1:bQ1Gvah4Sp8z7epSkgJaNTuZm7sutfA6Fji2/7cKFMc= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0/go.mod h1:9b8Q9rH52NgYH3ShiTFB5wf18Vt3RTH/VMB7LDcC1ug= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc= +go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 h1:oPW/SRFyHgIgxrvNhSBzqvZER2N5kRlci3/rGTOuyWo= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0/go.mod h1:B9Oka5QVD0bnmZNO6gBbBta6nohD/1Z+f9waH2oXyBs= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 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/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 h1:9PgnL3QNlj10uGxExowIDIZu66aVBwWhXmbOp1pa6RA= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0/go.mod h1:0ineDcLELf6JmKfuo0wvvhAVMuxWFYvkTin2iV4ydPQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/exporters/prometheus v0.59.0 h1:HHf+wKS6o5++XZhS98wvILrLVgHxjA/AMjqHKes+uzo= go.opentelemetry.io/otel/exporters/prometheus v0.59.0/go.mod h1:R8GpRXTZrqvXHDEGVH5bF6+JqAZcK8PjJcZ5nGhEWiE= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJhSENRg+Vvx+ynNilV8twBLBsXMY= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0= go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= -go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= -gocloud.dev v0.42.0 h1:qzG+9ItUL3RPB62/Amugws28n+4vGZXEoJEAMfjutzw= -gocloud.dev v0.42.0/go.mod h1:zkaYAapZfQisXOA4bzhsbA4ckiStGQ3Psvs9/OQ5dPM= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M= +gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1298,15 +1324,11 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 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= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1317,8 +1339,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1344,18 +1366,14 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1397,7 +1415,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1405,13 +1422,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1434,8 +1446,8 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1448,12 +1460,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1476,7 +1484,6 @@ golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1528,7 +1535,6 @@ golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1541,26 +1547,17 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1570,17 +1567,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1639,13 +1632,10 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= +golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1699,8 +1689,8 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= -google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= -google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= +google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1788,13 +1778,14 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= +google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1825,8 +1816,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1842,8 +1833,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= @@ -1856,8 +1847,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= -gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -1868,9 +1857,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= -gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= -gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/telebot.v3 v3.3.8 h1:uVDGjak9l824FN9YARWUHMsiNZnlohAVwUycw21k6t8= +gopkg.in/telebot.v3 v3.3.8/go.mod h1:1mlbqcLTVSfK9dx7fdp+Nb5HZsy4LLPtpZTKmwhwtzM= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1894,46 +1882,48 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= -k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= -k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= -k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= -k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= -k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.33.3 h1:7cQWC+GSH211NgY8LRKjBXNtkzra5SkpYzeZrOt5D+8= -k8s.io/kms v0.33.3/go.mod h1:C1I8mjFFBNzfUZXYt9FZVJ8MJl7ynFbGgZFbBzkBJ3E= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= -k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= -modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= -modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY= -modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= -modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +k8s.io/kms v0.34.1 h1:iCFOvewDPzWM9fMTfyIPO+4MeuZ0tcZbugxLNSHFG4w= +k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= -modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= -modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= -modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4= +modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= @@ -1945,14 +1935,12 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUo sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= -sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= -xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= -xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= +xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= diff --git a/apps/advisor/kinds/check.cue b/apps/advisor/kinds/check.cue index 01982443ffc..bf227efbe53 100644 --- a/apps/advisor/kinds/check.cue +++ b/apps/advisor/kinds/check.cue @@ -1,57 +1,49 @@ package advisor -check: { - kind: "Check" - pluralName: "Checks" - current: "v0alpha1" +checkv0alpha1: { + kind: "Check" + plural: "checks" + scope: "Namespaced" validation: { operations: [ "CREATE", "UPDATE", ] } - versions: { - "v0alpha1": { - codegen: { - ts: {enabled: false} - go: {enabled: true} - } - schema: { - #Data: { - // Generic data input that a check can receive - data?: [string]: string - } - #ErrorLink: { - // URL to a page with more information about the error - url: string - // Human readable error message - message: string - } - #ReportFailure: { - // Severity of the failure - severity: "high" | "low" - // Step ID that the failure is associated with - stepID: string - // Human readable identifier of the item that failed - item: string - // ID of the item that failed - itemID: string - // Links to actions that can be taken to resolve the failure - links: [...#ErrorLink] - // More information about the failure, not meant to be displayed to the user. Used for LLM suggestions. - moreInfo?: string - } - #Report: { - // Number of elements analyzed - count: int - // List of failures - failures: [...#ReportFailure] - } - spec: #Data - status: { - report: #Report - } - } + schema: { + #Data: { + // Generic data input that a check can receive + data?: [string]: string + } + #ErrorLink: { + // URL to a page with more information about the error + url: string + // Human readable error message + message: string + } + #ReportFailure: { + // Severity of the failure + severity: "high" | "low" + // Step ID that the failure is associated with + stepID: string + // Human readable identifier of the item that failed + item: string + // ID of the item that failed + itemID: string + // Links to actions that can be taken to resolve the failure + links: [...#ErrorLink] + // More information about the failure, not meant to be displayed to the user. Used for LLM suggestions. + moreInfo?: string + } + #Report: { + // Number of elements analyzed + count: int + // List of failures + failures: [...#ReportFailure] + } + spec: #Data + status: { + report: #Report } } } diff --git a/apps/advisor/kinds/checktype.cue b/apps/advisor/kinds/checktype.cue index 315d231754d..25902d8265a 100644 --- a/apps/advisor/kinds/checktype.cue +++ b/apps/advisor/kinds/checktype.cue @@ -1,27 +1,19 @@ package advisor -checktype: { - kind: "CheckType" - pluralName: "CheckTypes" - current: "v0alpha1" - versions: { - "v0alpha1": { - codegen: { - ts: {enabled: false} - go: {enabled: true} - } - schema: { - #Step: { - title: string - description: string - stepID: string - resolution: string - } - spec: { - name: string - steps: [...#Step] - } - } +checktypev0alpha1: { + kind: "CheckType" + plural: "checktypes" + scope: "Namespaced" + schema: { + #Step: { + title: string + description: string + stepID: string + resolution: string + } + spec: { + name: string + steps: [...#Step] } } } diff --git a/apps/advisor/kinds/manifest.cue b/apps/advisor/kinds/manifest.cue index 75a0eee05ef..ca791996f13 100644 --- a/apps/advisor/kinds/manifest.cue +++ b/apps/advisor/kinds/manifest.cue @@ -1,10 +1,18 @@ package advisor manifest: { - appName: "advisor" - groupOverride: "advisor.grafana.app" - kinds: [ - check, - checktype, - ] + appName: "advisor" + groupOverride: "advisor.grafana.app" + versions: { + "v0alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + checkv0alpha1, + checktypev0alpha1, + ] + } + } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_client_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_client_gen.go new file mode 100644 index 00000000000..b224a1d4478 --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type CheckClient struct { + client *resource.TypedClient[*Check, *CheckList] +} + +func NewCheckClient(client resource.Client) *CheckClient { + return &CheckClient{ + client: resource.NewTypedClient[*Check, *CheckList](client, CheckKind()), + } +} + +func NewCheckClientFromGenerator(generator resource.ClientGenerator) (*CheckClient, error) { + c, err := generator.ClientFor(CheckKind()) + if err != nil { + return nil, err + } + return NewCheckClient(c), nil +} + +func (c *CheckClient) Get(ctx context.Context, identifier resource.Identifier) (*Check, error) { + return c.client.Get(ctx, identifier) +} + +func (c *CheckClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*CheckList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *CheckClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*CheckList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *CheckClient) Create(ctx context.Context, obj *Check, opts resource.CreateOptions) (*Check, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = CheckKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *CheckClient) Update(ctx context.Context, obj *Check, opts resource.UpdateOptions) (*Check, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *CheckClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Check, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *CheckClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus CheckStatus, opts resource.UpdateOptions) (*Check, error) { + return c.client.Update(ctx, &Check{ + TypeMeta: metav1.TypeMeta{ + Kind: CheckKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *CheckClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_client_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_client_gen.go new file mode 100644 index 00000000000..2acf5bf5cfe --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type CheckTypeClient struct { + client *resource.TypedClient[*CheckType, *CheckTypeList] +} + +func NewCheckTypeClient(client resource.Client) *CheckTypeClient { + return &CheckTypeClient{ + client: resource.NewTypedClient[*CheckType, *CheckTypeList](client, CheckTypeKind()), + } +} + +func NewCheckTypeClientFromGenerator(generator resource.ClientGenerator) (*CheckTypeClient, error) { + c, err := generator.ClientFor(CheckTypeKind()) + if err != nil { + return nil, err + } + return NewCheckTypeClient(c), nil +} + +func (c *CheckTypeClient) Get(ctx context.Context, identifier resource.Identifier) (*CheckType, error) { + return c.client.Get(ctx, identifier) +} + +func (c *CheckTypeClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*CheckTypeList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *CheckTypeClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*CheckTypeList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *CheckTypeClient) Create(ctx context.Context, obj *CheckType, opts resource.CreateOptions) (*CheckType, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = CheckTypeKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *CheckTypeClient) Update(ctx context.Context, obj *CheckType, opts resource.UpdateOptions) (*CheckType, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *CheckTypeClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*CheckType, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *CheckTypeClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus CheckTypeStatus, opts resource.UpdateOptions) (*CheckType, error) { + return c.client.Update(ctx, &CheckType{ + TypeMeta: metav1.TypeMeta{ + Kind: CheckTypeKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *CheckTypeClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index a85ff8b904f..5c7b5896ee8 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -12,22 +12,26 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" v0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" ) var ( - rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object"}}`) + rawSchemaCheckv0alpha1 = []byte(`{"Check":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"ErrorLink":{"additionalProperties":false,"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Report":{"additionalProperties":false,"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"$ref":"#/components/schemas/ReportFailure"},"type":"array"}},"required":["count","failures"],"type":"object"},"ReportFailure":{"additionalProperties":false,"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"$ref":"#/components/schemas/ErrorLink"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"$ref":"#/components/schemas/Report"}},"required":["report"],"type":"object"}}`) versionSchemaCheckv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaCheckv0alpha1, &versionSchemaCheckv0alpha1) - rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaCheckTypev0alpha1 = []byte(`{"CheckType":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Step":{"additionalProperties":false,"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"name":{"type":"string"},"steps":{"items":{"$ref":"#/components/schemas/Step"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaCheckTypev0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaCheckTypev0alpha1, &versionSchemaCheckTypev0alpha1) ) var appManifestData = app.ManifestData{ - AppName: "advisor", - Group: "advisor.grafana.app", + AppName: "advisor", + Group: "advisor.grafana.app", + PreferredVersion: "v0alpha1", Versions: []app.ManifestVersion{ { Name: "v0alpha1", @@ -57,6 +61,11 @@ var appManifestData = app.ManifestData{ Schema: &versionSchemaCheckTypev0alpha1, }, }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + Schemas: map[string]spec.Schema{}, + }, }, }, } @@ -86,6 +95,7 @@ var customRouteToGoResponseType = map[string]any{} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. // If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { if len(path) > 0 && path[0] == '/' { path = path[1:] @@ -93,3 +103,42 @@ func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (g goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] return goType, exists } + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/advisor/pkg/app/checkregistry/mockchecks/checkregistry.go b/apps/advisor/pkg/app/checkregistry/mockchecks/checkregistry.go new file mode 100644 index 00000000000..e1b9cf88bf4 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/mockchecks/checkregistry.go @@ -0,0 +1,59 @@ +package mockchecks + +import ( + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/plugincheck" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/repo" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +// mockchecks.CheckRegistry is a mock implementation of the checkregistry.CheckService interface +// TODO: Add mocked checks here +type CheckRegistry struct { + datasourceSvc datasources.DataSourceService + pluginStore pluginstore.Store + pluginClient plugins.Client + pluginRepo repo.Service + GrafanaVersion string + pluginContextProvider datasourcecheck.PluginContextProvider + updateChecker pluginchecker.PluginUpdateChecker + pluginErrorResolver plugins.ErrorResolver +} + +func (m *CheckRegistry) Checks() []checks.Check { + return []checks.Check{ + datasourcecheck.New( + m.datasourceSvc, + m.pluginStore, + m.pluginContextProvider, + m.pluginClient, + m.pluginRepo, + m.GrafanaVersion, + ), + plugincheck.New( + m.pluginStore, + m.pluginRepo, + m.updateChecker, + m.pluginErrorResolver, + m.GrafanaVersion, + ), + } +} + +func New() *CheckRegistry { + return &CheckRegistry{ + datasourceSvc: &mocksvcs.DatasourceSvc{}, + pluginStore: &mocksvcs.PluginStore{}, + pluginClient: &mocksvcs.PluginClient{}, + pluginRepo: &mocksvcs.PluginRepo{}, + pluginContextProvider: &mocksvcs.PluginContextProvider{}, + updateChecker: &mocksvcs.UpdateChecker{}, + pluginErrorResolver: &mocksvcs.PluginErrorResolver{}, + GrafanaVersion: "1.0.0", + } +} diff --git a/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/datasourcesvc.go b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/datasourcesvc.go new file mode 100644 index 00000000000..73122e53adb --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/datasourcesvc.go @@ -0,0 +1,44 @@ +package mocksvcs + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/datasources" +) + +var dss = map[string]*datasources.DataSource{ + "prometheus-uid": { + ID: 1, + UID: "prometheus-uid", + Name: "Prometheus", + Type: "prometheus", + }, + "mysql-uid": { + ID: 2, + UID: "mysql-uid", + Name: "MySQL", + Type: "mysql", + }, + "unknown-uid": { + ID: 3, + UID: "unknown-uid", + Name: "Unknown", + Type: "unknown", + }, +} + +type DatasourceSvc struct { + datasources.DataSourceService +} + +func (m *DatasourceSvc) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) { + sources := make([]*datasources.DataSource, 0, len(dss)) + for _, ds := range dss { + sources = append(sources, ds) + } + return sources, nil +} + +func (m *DatasourceSvc) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) { + return dss[query.UID], nil +} diff --git a/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginclient.go b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginclient.go new file mode 100644 index 00000000000..a8ce175a687 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginclient.go @@ -0,0 +1,19 @@ +package mocksvcs + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/plugins" +) + +type PluginClient struct { + plugins.Client +} + +func (m *PluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "Plugin is healthy", + }, nil +} diff --git a/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/plugincontextprovider.go b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/plugincontextprovider.go new file mode 100644 index 00000000000..b5bb6f88e8e --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/plugincontextprovider.go @@ -0,0 +1,53 @@ +package mocksvcs + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/datasources" +) + +type PluginContextProvider struct { +} + +// ACTUALLY USED by datasourcecheck +func (m *PluginContextProvider) GetWithDataSource(ctx context.Context, pluginID string, user identity.Requester, ds *datasources.DataSource) (backend.PluginContext, error) { + // Create a plugin context with sample data based on the datasource + pluginContext := backend.PluginContext{ + PluginID: pluginID, + PluginVersion: "1.0.0", + OrgID: 1, + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + ID: ds.ID, + UID: ds.UID, + Name: ds.Name, + URL: ds.URL, + JSONData: []byte(`{ + "httpMethod": "GET", + "timeout": "30s", + "keepCookies": [] + }`), + DecryptedSecureJSONData: map[string]string{ + "password": "sample-password", + "apiKey": "sample-api-key", + }, + }, + GrafanaConfig: backend.NewGrafanaCfg(map[string]string{ + "app_url": "http://localhost:3000", + "default_timezone": "UTC", + }), + } + + // Add user context if provided + if user != nil && !user.IsNil() { + pluginContext.User = &backend.User{ + Login: user.GetLogin(), + Name: user.GetName(), + Email: user.GetEmail(), + Role: string(user.GetOrgRole()), + } + } + + return pluginContext, nil +} diff --git a/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginerrorresolver.go b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginerrorresolver.go new file mode 100644 index 00000000000..db545827991 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginerrorresolver.go @@ -0,0 +1,19 @@ +package mocksvcs + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" +) + +type PluginErrorResolver struct { +} + +// Assume no plugin with errors +func (m *PluginErrorResolver) PluginErrors(ctx context.Context) []*plugins.Error { + return nil +} + +func (m *PluginErrorResolver) PluginError(ctx context.Context, pluginID string) *plugins.Error { + return nil +} diff --git a/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginrepo.go b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginrepo.go new file mode 100644 index 00000000000..0ab8d225314 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginrepo.go @@ -0,0 +1,26 @@ +package mocksvcs + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins/repo" +) + +type PluginRepo struct { + repo.Service +} + +func (m *PluginRepo) GetPluginsInfo(ctx context.Context, options repo.GetPluginsInfoOptions, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error) { + return []repo.PluginInfo{ + { + ID: 1, + Slug: "grafana-piechart-panel", + Version: "1.6.0", + }, + { + ID: 2, + Slug: "prometheus", + Version: "10.0.0", + }, + }, nil +} diff --git a/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginstore.go b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginstore.go new file mode 100644 index 00000000000..782d93024f3 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/pluginstore.go @@ -0,0 +1,114 @@ +package mocksvcs + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +type PluginStore struct { +} + +var ps = map[string]pluginstore.Plugin{ + "prometheus": { + JSONData: plugins.JSONData{ + ID: "prometheus", + Type: plugins.TypeDataSource, + Name: "Prometheus", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + }, + Version: "10.0.0", + }, + Category: "Time series databases", + State: plugins.ReleaseStateAlpha, + Backend: true, + Metrics: true, + Logs: true, + Alerting: true, + Explore: true, + }, + Class: plugins.ClassCore, + Signature: plugins.SignatureStatusInternal, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "grafana.com", + }, + "test-datasource": { + JSONData: plugins.JSONData{ + ID: "grafana-piechart-panel", + Type: plugins.TypePanel, + Name: "Pie Chart", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + }, + Version: "1.6.0", + }, + Category: "Visualization", + State: plugins.ReleaseStateAlpha, + }, + Class: plugins.ClassCore, + Signature: plugins.SignatureStatusInternal, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "grafana.com", + }, + "grafana-piechart-panel": { + JSONData: plugins.JSONData{ + ID: "prometheus", + Type: plugins.TypeDataSource, + Name: "Prometheus", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + }, + Version: "10.0.0", + }, + Category: "Time series databases", + State: plugins.ReleaseStateAlpha, + Backend: true, + Metrics: true, + Logs: true, + Alerting: true, + Explore: true, + }, + Class: plugins.ClassCore, + Signature: plugins.SignatureStatusInternal, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "grafana.com", + }, + "test-app": { + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.TypeApp, + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Author", + }, + Version: "2.0.0", + }, + Category: "Application", + State: plugins.ReleaseStateAlpha, + AutoEnabled: true, + }, + Class: plugins.ClassExternal, + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypeCommercial, + SignatureOrg: "test.com", + }, +} + +func (s *PluginStore) Plugin(ctx context.Context, pluginID string) (pluginstore.Plugin, bool) { + p, ok := ps[pluginID] + return p, ok +} + +func (s *PluginStore) Plugins(ctx context.Context, pluginTypes ...plugins.Type) []pluginstore.Plugin { + plugins := make([]pluginstore.Plugin, 0, len(ps)) + for _, p := range ps { + plugins = append(plugins, p) + } + return plugins +} diff --git a/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/updatechecker.go b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/updatechecker.go new file mode 100644 index 00000000000..efbeb217e74 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs/updatechecker.go @@ -0,0 +1,18 @@ +package mocksvcs + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +type UpdateChecker struct { +} + +func (m *UpdateChecker) IsUpdatable(ctx context.Context, plugin pluginstore.Plugin) bool { + return true +} + +func (m *UpdateChecker) CanUpdate(pluginId string, currentVersion string, targetVersion string, onlyMinor bool) bool { + return true +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index 524dae04268..cae29e181fd 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -26,7 +26,7 @@ const ( type check struct { DatasourceSvc datasources.DataSourceService PluginStore pluginstore.Store - PluginContextProvider pluginContextProvider + PluginContextProvider PluginContextProvider PluginClient plugins.Client PluginRepo repo.Service GrafanaVersion string @@ -37,7 +37,7 @@ type check struct { func New( datasourceSvc datasources.DataSourceService, pluginStore pluginstore.Store, - pluginContextProvider pluginContextProvider, + pluginContextProvider PluginContextProvider, pluginClient plugins.Client, pluginRepo repo.Service, grafanaVersion string, @@ -168,6 +168,6 @@ func (c *check) canBeInstalled(ctx context.Context, pluginType string) (bool, er return isAvailableInRepo, nil } -type pluginContextProvider interface { +type PluginContextProvider interface { GetWithDataSource(ctx context.Context, pluginID string, user identity.Requester, ds *datasources.DataSource) (backend.PluginContext, error) } diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/health_check_step.go b/apps/advisor/pkg/app/checks/datasourcecheck/health_check_step.go index c020d0ba82a..7e88e872e05 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/health_check_step.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/health_check_step.go @@ -15,7 +15,7 @@ import ( ) type healthCheckStep struct { - PluginContextProvider pluginContextProvider + PluginContextProvider PluginContextProvider PluginClient plugins.Client } diff --git a/apps/advisor/pkg/standalone/server.go b/apps/advisor/pkg/standalone/server.go new file mode 100644 index 00000000000..80dd82418ef --- /dev/null +++ b/apps/advisor/pkg/standalone/server.go @@ -0,0 +1,58 @@ +package main + +import ( + "log/slog" + "os" + + "k8s.io/apiserver/pkg/admission" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/client-go/rest" + "k8s.io/component-base/cli" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "github.com/grafana/grafana-app-sdk/k8s/apiserver/cmd/server" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/simple" + "github.com/grafana/grafana/apps/advisor/pkg/apis" + advisorapp "github.com/grafana/grafana/apps/advisor/pkg/app" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry/mockchecks" +) + +func main() { + logging.DefaultLogger = logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + provider := simple.NewAppProvider(apis.LocalManifest(), nil, advisorapp.New) + config := app.Config{ + KubeConfig: rest.Config{}, // this will be replaced by the apiserver loopback config + ManifestData: *apis.LocalManifest().ManifestData, + SpecificConfig: checkregistry.AdvisorAppConfig{ + CheckRegistry: mockchecks.New(), + PluginConfig: map[string]string{}, + StackID: "1", // Numeric stack ID for standalone mode + OrgService: nil, // Not needed when StackID is set + }, + } + installer, err := apiserver.NewDefaultAppInstaller(provider, config, &apis.GoTypeAssociator{}) + if err != nil { + panic(err) + } + ctx := genericapiserver.SetupSignalContext() + opts := apiserver.NewOptions([]apiserver.AppInstaller{installer}) + opts.RecommendedOptions.Authentication = nil + opts.RecommendedOptions.Authorization = nil + opts.RecommendedOptions.CoreAPI = nil + opts.RecommendedOptions.EgressSelector = nil + opts.RecommendedOptions.Admission.Plugins = admission.NewPlugins() + opts.RecommendedOptions.Admission.RecommendedPluginOrder = []string{} + opts.RecommendedOptions.Admission.EnablePlugins = []string{} + opts.RecommendedOptions.Features.EnablePriorityAndFairness = false + opts.RecommendedOptions.ExtraAdmissionInitializers = func(_ *genericapiserver.RecommendedConfig) ([]admission.PluginInitializer, error) { + return nil, nil + } + cmd := server.NewCommandStartServer(ctx, opts) + code := cli.Run(cmd) + os.Exit(code) +} diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index 008fa603189..efab71e825e 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 + github.com/grafana/grafana-app-sdk v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -27,7 +27,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/text v0.30.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index 16227da2710..2d95fb4b1a2 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -23,8 +23,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/apps/alerting/notifications/Makefile b/apps/alerting/notifications/Makefile index 039a81a6f6e..9564d011d14 100644 --- a/apps/alerting/notifications/Makefile +++ b/apps/alerting/notifications/Makefile @@ -1,8 +1,11 @@ include ../../sdk.mk -.PHONY: generate -generate: do-generate ## Run Grafana App SDK code generation - -.PHONY: do-generate -do-generate: install-app-sdk update-app-sdk - @$(APP_SDK_BIN) generate --grouping=group --gogenpath=./pkg/apis --defencoding=yaml --postprocess +.PHONY: generate # Run Grafana App SDK code generation +generate: install-app-sdk update-app-sdk + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --genoperatorstate=false \ + --defencoding=none + \ No newline at end of file diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 91c7b67b93b..b4dfcef1c01 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 k8s.io/apimachinery v0.34.1 k8s.io/apiserver v0.34.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -84,13 +84,13 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 620e474799c..c8c9f53e5ef 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -71,10 +71,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= @@ -224,8 +224,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -236,8 +236,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= @@ -254,21 +254,21 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_client_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_client_gen.go index 057453e2257..1e41e0e09df 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_client_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ReceiverClient struct { @@ -76,24 +75,6 @@ func (c *ReceiverClient) Patch(ctx context.Context, identifier resource.Identifi return c.client.Patch(ctx, identifier, req, opts) } -func (c *ReceiverClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ReceiverStatus, opts resource.UpdateOptions) (*Receiver, error) { - return c.client.Update(ctx, &Receiver{ - TypeMeta: metav1.TypeMeta{ - Kind: ReceiverKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *ReceiverClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go index 3ff0363cc88..00bb1a34745 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go @@ -21,8 +21,6 @@ type Receiver struct { // Spec is the spec of the Receiver Spec ReceiverSpec `json:"spec" yaml:"spec"` - - Status ReceiverStatus `json:"status" yaml:"status"` } func (o *Receiver) GetSpec() any { @@ -39,15 +37,11 @@ func (o *Receiver) SetSpec(spec any) error { } func (o *Receiver) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *Receiver) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *Receiver) GetSubresource(name string) (any, bool) { func (o *Receiver) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(ReceiverStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type ReceiverStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *Receiver) DeepCopyInto(dst *Receiver) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *ReceiverSpec) DeepCopy() *ReceiverSpec { func (s *ReceiverSpec) DeepCopyInto(dst *ReceiverSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of ReceiverStatus -func (s *ReceiverStatus) DeepCopy() *ReceiverStatus { - cpy := &ReceiverStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies ReceiverStatus into another ReceiverStatus object -func (s *ReceiverStatus) DeepCopyInto(dst *ReceiverStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_client_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_client_gen.go index da611aa591a..27d605e1898 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_client_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type RoutingTreeClient struct { @@ -76,24 +75,6 @@ func (c *RoutingTreeClient) Patch(ctx context.Context, identifier resource.Ident return c.client.Patch(ctx, identifier, req, opts) } -func (c *RoutingTreeClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus RoutingTreeStatus, opts resource.UpdateOptions) (*RoutingTree, error) { - return c.client.Update(ctx, &RoutingTree{ - TypeMeta: metav1.TypeMeta{ - Kind: RoutingTreeKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *RoutingTreeClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go index f51fc9732a9..e59f0dada5c 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go @@ -21,8 +21,6 @@ type RoutingTree struct { // Spec is the spec of the RoutingTree Spec RoutingTreeSpec `json:"spec" yaml:"spec"` - - Status RoutingTreeStatus `json:"status" yaml:"status"` } func (o *RoutingTree) GetSpec() any { @@ -39,15 +37,11 @@ func (o *RoutingTree) SetSpec(spec any) error { } func (o *RoutingTree) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *RoutingTree) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *RoutingTree) GetSubresource(name string) (any, bool) { func (o *RoutingTree) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(RoutingTreeStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type RoutingTreeStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *RoutingTree) DeepCopyInto(dst *RoutingTree) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *RoutingTreeSpec) DeepCopy() *RoutingTreeSpec { func (s *RoutingTreeSpec) DeepCopyInto(dst *RoutingTreeSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of RoutingTreeStatus -func (s *RoutingTreeStatus) DeepCopy() *RoutingTreeStatus { - cpy := &RoutingTreeStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies RoutingTreeStatus into another RoutingTreeStatus object -func (s *RoutingTreeStatus) DeepCopyInto(dst *RoutingTreeStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_client_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_client_gen.go index 54bbfb9ceb2..265021f4b71 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_client_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type TemplateGroupClient struct { @@ -76,24 +75,6 @@ func (c *TemplateGroupClient) Patch(ctx context.Context, identifier resource.Ide return c.client.Patch(ctx, identifier, req, opts) } -func (c *TemplateGroupClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus TemplateGroupStatus, opts resource.UpdateOptions) (*TemplateGroup, error) { - return c.client.Update(ctx, &TemplateGroup{ - TypeMeta: metav1.TypeMeta{ - Kind: TemplateGroupKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *TemplateGroupClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go index 459b07343c5..0866bcb258c 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go @@ -21,8 +21,6 @@ type TemplateGroup struct { // Spec is the spec of the TemplateGroup Spec TemplateGroupSpec `json:"spec" yaml:"spec"` - - Status TemplateGroupStatus `json:"status" yaml:"status"` } func (o *TemplateGroup) GetSpec() any { @@ -39,15 +37,11 @@ func (o *TemplateGroup) SetSpec(spec any) error { } func (o *TemplateGroup) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *TemplateGroup) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *TemplateGroup) GetSubresource(name string) (any, bool) { func (o *TemplateGroup) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(TemplateGroupStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type TemplateGroupStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *TemplateGroup) DeepCopyInto(dst *TemplateGroup) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *TemplateGroupSpec) DeepCopy() *TemplateGroupSpec { func (s *TemplateGroupSpec) DeepCopyInto(dst *TemplateGroupSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of TemplateGroupStatus -func (s *TemplateGroupStatus) DeepCopy() *TemplateGroupStatus { - cpy := &TemplateGroupStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies TemplateGroupStatus into another TemplateGroupStatus object -func (s *TemplateGroupStatus) DeepCopyInto(dst *TemplateGroupStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_status_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_status_gen.go deleted file mode 100644 index 8312358cda5..00000000000 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_status_gen.go +++ /dev/null @@ -1,44 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// +k8s:openapi-gen=true -type TemplateGroupstatusOperatorState struct { - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State TemplateGroupStatusOperatorStateState `json:"state"` - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` -} - -// NewTemplateGroupstatusOperatorState creates a new TemplateGroupstatusOperatorState object. -func NewTemplateGroupstatusOperatorState() *TemplateGroupstatusOperatorState { - return &TemplateGroupstatusOperatorState{} -} - -// +k8s:openapi-gen=true -type TemplateGroupStatus struct { - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]TemplateGroupstatusOperatorState `json:"operatorStates,omitempty"` - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` -} - -// NewTemplateGroupStatus creates a new TemplateGroupStatus object. -func NewTemplateGroupStatus() *TemplateGroupStatus { - return &TemplateGroupStatus{} -} - -// +k8s:openapi-gen=true -type TemplateGroupStatusOperatorStateState string - -const ( - TemplateGroupStatusOperatorStateStateSuccess TemplateGroupStatusOperatorStateState = "success" - TemplateGroupStatusOperatorStateStateInProgress TemplateGroupStatusOperatorStateState = "in_progress" - TemplateGroupStatusOperatorStateStateFailed TemplateGroupStatusOperatorStateState = "failed" -) diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_client_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_client_gen.go index afa269ac96f..09ddb684613 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_client_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type TimeIntervalClient struct { @@ -76,24 +75,6 @@ func (c *TimeIntervalClient) Patch(ctx context.Context, identifier resource.Iden return c.client.Patch(ctx, identifier, req, opts) } -func (c *TimeIntervalClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus TimeIntervalStatus, opts resource.UpdateOptions) (*TimeInterval, error) { - return c.client.Update(ctx, &TimeInterval{ - TypeMeta: metav1.TypeMeta{ - Kind: TimeIntervalKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *TimeIntervalClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go index b4646bb8422..0ef813ee40c 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go @@ -21,8 +21,6 @@ type TimeInterval struct { // Spec is the spec of the TimeInterval Spec TimeIntervalSpec `json:"spec" yaml:"spec"` - - Status TimeIntervalStatus `json:"status" yaml:"status"` } func (o *TimeInterval) GetSpec() any { @@ -39,15 +37,11 @@ func (o *TimeInterval) SetSpec(spec any) error { } func (o *TimeInterval) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *TimeInterval) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *TimeInterval) GetSubresource(name string) (any, bool) { func (o *TimeInterval) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(TimeIntervalStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type TimeIntervalStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *TimeInterval) DeepCopyInto(dst *TimeInterval) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *TimeIntervalSpec) DeepCopy() *TimeIntervalSpec { func (s *TimeIntervalSpec) DeepCopyInto(dst *TimeIntervalSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of TimeIntervalStatus -func (s *TimeIntervalStatus) DeepCopy() *TimeIntervalStatus { - cpy := &TimeIntervalStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies TimeIntervalStatus into another TimeIntervalStatus object -func (s *TimeIntervalStatus) DeepCopyInto(dst *TimeIntervalStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_status_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_status_gen.go deleted file mode 100644 index a6128624acd..00000000000 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_status_gen.go +++ /dev/null @@ -1,44 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// +k8s:openapi-gen=true -type TimeIntervalstatusOperatorState struct { - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State TimeIntervalStatusOperatorStateState `json:"state"` - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` -} - -// NewTimeIntervalstatusOperatorState creates a new TimeIntervalstatusOperatorState object. -func NewTimeIntervalstatusOperatorState() *TimeIntervalstatusOperatorState { - return &TimeIntervalstatusOperatorState{} -} - -// +k8s:openapi-gen=true -type TimeIntervalStatus struct { - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]TimeIntervalstatusOperatorState `json:"operatorStates,omitempty"` - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` -} - -// NewTimeIntervalStatus creates a new TimeIntervalStatus object. -func NewTimeIntervalStatus() *TimeIntervalStatus { - return &TimeIntervalStatus{} -} - -// +k8s:openapi-gen=true -type TimeIntervalStatusOperatorStateState string - -const ( - TimeIntervalStatusOperatorStateStateSuccess TimeIntervalStatusOperatorStateState = "success" - TimeIntervalStatusOperatorStateStateInProgress TimeIntervalStatusOperatorStateState = "in_progress" - TimeIntervalStatusOperatorStateStateFailed TimeIntervalStatusOperatorStateState = "failed" -) diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go deleted file mode 100644 index 782a0d1ca54..00000000000 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go +++ /dev/null @@ -1,1321 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// Code generated by grafana-app-sdk. DO NOT EDIT. - -package v0alpha1 - -import ( - common "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" -) - -func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver": schema_pkg_apis_alertingnotifications_v0alpha1_Receiver(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverIntegration(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverList": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverList(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverSpec(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverStatus": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverStatus(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverstatusOperatorState": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverstatusOperatorState(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTree(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeList": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeList(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeMatcher(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRoute(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRouteDefaults(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeSpec(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeStatus": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeStatus(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreestatusOperatorState": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreestatusOperatorState(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroup(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupList": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupList(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupSpec(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupStatus": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupStatus(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupstatusOperatorState": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupstatusOperatorState(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval": schema_pkg_apis_alertingnotifications_v0alpha1_TimeInterval(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalInterval(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalList(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalSpec(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalStatus": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalStatus(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalTimeRange(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalstatusOperatorState": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalstatusOperatorState(ref), - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_Receiver(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "Spec is the spec of the Receiver", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverStatus"), - }, - }, - }, - Required: []string{"metadata", "spec", "status"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverIntegration(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "uid": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "type": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "version": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "disableResolveMessage": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - "settings": { - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - "secureFields": { - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: false, - Type: []string{"boolean"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"type", "version", "settings"}, - }, - }, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver"), - }, - }, - }, - }, - }, - }, - Required: []string{"metadata", "items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "title": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "integrations": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration"), - }, - }, - }, - }, - }, - }, - Required: []string{"title", "integrations"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "operatorStates": { - SchemaProps: spec.SchemaProps{ - Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverstatusOperatorState"), - }, - }, - }, - }, - }, - "additionalFields": { - SchemaProps: spec.SchemaProps{ - Description: "additionalFields is reserved for future use", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverstatusOperatorState"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "descriptiveState": { - SchemaProps: spec.SchemaProps{ - Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", - Type: []string{"string"}, - Format: "", - }, - }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "details contains any extra information that is operator-specific", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"lastEvaluation", "state"}, - }, - }, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTree(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "Spec is the spec of the RoutingTree", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeStatus"), - }, - }, - }, - Required: []string{"metadata", "spec", "status"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree"), - }, - }, - }, - }, - }, - }, - Required: []string{"metadata", "items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeMatcher(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "type": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "label": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "value": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"type", "label", "value"}, - }, - }, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRoute(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "receiver": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "matchers": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher"), - }, - }, - }, - }, - }, - "continue": { - SchemaProps: spec.SchemaProps{ - Default: false, - Type: []string{"boolean"}, - Format: "", - }, - }, - "group_by": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "mute_time_intervals": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "active_time_intervals": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "routes": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"), - }, - }, - }, - }, - }, - "group_wait": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "group_interval": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "repeat_interval": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"continue"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRouteDefaults(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "receiver": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "group_by": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "group_wait": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "group_interval": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "repeat_interval": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"receiver"}, - }, - }, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "defaults": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults"), - }, - }, - "routes": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"), - }, - }, - }, - }, - }, - }, - Required: []string{"defaults", "routes"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "operatorStates": { - SchemaProps: spec.SchemaProps{ - Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreestatusOperatorState"), - }, - }, - }, - }, - }, - "additionalFields": { - SchemaProps: spec.SchemaProps{ - Description: "additionalFields is reserved for future use", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreestatusOperatorState"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreestatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "descriptiveState": { - SchemaProps: spec.SchemaProps{ - Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", - Type: []string{"string"}, - Format: "", - }, - }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "details contains any extra information that is operator-specific", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"lastEvaluation", "state"}, - }, - }, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "Spec is the spec of the TemplateGroup", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupStatus"), - }, - }, - }, - Required: []string{"metadata", "spec", "status"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup"), - }, - }, - }, - }, - }, - }, - Required: []string{"metadata", "items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "title": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "content": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"title", "content"}, - }, - }, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "operatorStates": { - SchemaProps: spec.SchemaProps{ - Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupstatusOperatorState"), - }, - }, - }, - }, - }, - "additionalFields": { - SchemaProps: spec.SchemaProps{ - Description: "additionalFields is reserved for future use", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupstatusOperatorState"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "descriptiveState": { - SchemaProps: spec.SchemaProps{ - Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", - Type: []string{"string"}, - Format: "", - }, - }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "details contains any extra information that is operator-specific", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"lastEvaluation", "state"}, - }, - }, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "Spec is the spec of the TimeInterval", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalStatus"), - }, - }, - }, - Required: []string{"metadata", "spec", "status"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "times": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange"), - }, - }, - }, - }, - }, - "weekdays": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "days_of_month": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "months": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "years": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "location": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval"), - }, - }, - }, - }, - }, - }, - Required: []string{"metadata", "items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "name": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "time_intervals": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval"), - }, - }, - }, - }, - }, - }, - Required: []string{"name", "time_intervals"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "operatorStates": { - SchemaProps: spec.SchemaProps{ - Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalstatusOperatorState"), - }, - }, - }, - }, - }, - "additionalFields": { - SchemaProps: spec.SchemaProps{ - Description: "additionalFields is reserved for future use", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalstatusOperatorState"}, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalTimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "start_time": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "end_time": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"start_time", "end_time"}, - }, - }, - } -} - -func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "descriptiveState": { - SchemaProps: spec.SchemaProps{ - Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", - Type: []string{"string"}, - Format: "", - }, - }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "details contains any extra information that is operator-specific", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"lastEvaluation", "state"}, - }, - }, - } -} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go index 97b11485dd1..cebfbec12f1 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go @@ -20,16 +20,16 @@ import ( ) var ( - rawSchemaReceiverv0alpha1 = []byte(`{"Integration":{"additionalProperties":false,"properties":{"disableResolveMessage":{"type":"boolean"},"secureFields":{"additionalProperties":{"type":"boolean"},"type":"object"},"settings":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"type":{"type":"string"},"uid":{"type":"string"},"version":{"type":"string"}},"required":["type","version","settings"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Receiver":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"integrations":{"items":{"$ref":"#/components/schemas/Integration"},"type":"array"},"title":{"type":"string"}},"required":["title","integrations"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaReceiverv0alpha1 = []byte(`{"Integration":{"additionalProperties":false,"properties":{"disableResolveMessage":{"type":"boolean"},"secureFields":{"additionalProperties":{"type":"boolean"},"type":"object"},"settings":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"type":{"type":"string"},"uid":{"type":"string"},"version":{"type":"string"}},"required":["type","version","settings"],"type":"object"},"Receiver":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"integrations":{"items":{"$ref":"#/components/schemas/Integration"},"type":"array"},"title":{"type":"string"}},"required":["title","integrations"],"type":"object"}}`) versionSchemaReceiverv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaReceiverv0alpha1, &versionSchemaReceiverv0alpha1) - rawSchemaRoutingTreev0alpha1 = []byte(`{"Matcher":{"additionalProperties":false,"properties":{"label":{"type":"string"},"type":{"enum":["=","!=","=~","!~"],"type":"string"},"value":{"type":"string"}},"required":["type","label","value"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Route":{"additionalProperties":false,"properties":{"active_time_intervals":{"items":{"type":"string"},"type":"array"},"continue":{"type":"boolean"},"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"matchers":{"items":{"$ref":"#/components/schemas/Matcher"},"type":"array"},"mute_time_intervals":{"items":{"type":"string"},"type":"array"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["continue"],"type":"object"},"RouteDefaults":{"additionalProperties":false,"properties":{"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"}},"required":["receiver"],"type":"object"},"RoutingTree":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"defaults":{"$ref":"#/components/schemas/RouteDefaults"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["defaults","routes"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaRoutingTreev0alpha1 = []byte(`{"Matcher":{"additionalProperties":false,"properties":{"label":{"type":"string"},"type":{"enum":["=","!=","=~","!~"],"type":"string"},"value":{"type":"string"}},"required":["type","label","value"],"type":"object"},"Route":{"additionalProperties":false,"properties":{"active_time_intervals":{"items":{"type":"string"},"type":"array"},"continue":{"type":"boolean"},"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"matchers":{"items":{"$ref":"#/components/schemas/Matcher"},"type":"array"},"mute_time_intervals":{"items":{"type":"string"},"type":"array"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["continue"],"type":"object"},"RouteDefaults":{"additionalProperties":false,"properties":{"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"}},"required":["receiver"],"type":"object"},"RoutingTree":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"defaults":{"$ref":"#/components/schemas/RouteDefaults"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["defaults","routes"],"type":"object"}}`) versionSchemaRoutingTreev0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaRoutingTreev0alpha1, &versionSchemaRoutingTreev0alpha1) - rawSchemaTemplateGroupv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"title":{"type":"string"}},"required":["title","content"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaTemplateGroupv0alpha1 = []byte(`{"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"title":{"type":"string"}},"required":["title","content"],"type":"object"}}`) versionSchemaTemplateGroupv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaTemplateGroupv0alpha1, &versionSchemaTemplateGroupv0alpha1) - rawSchemaTimeIntervalv0alpha1 = []byte(`{"Interval":{"additionalProperties":false,"properties":{"days_of_month":{"items":{"type":"string"},"type":"array"},"location":{"type":"string"},"months":{"items":{"type":"string"},"type":"array"},"times":{"items":{"$ref":"#/components/schemas/TimeRange"},"type":"array"},"weekdays":{"items":{"type":"string"},"type":"array"},"years":{"items":{"type":"string"},"type":"array"}},"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"TimeInterval":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"TimeRange":{"additionalProperties":false,"properties":{"end_time":{"type":"string"},"start_time":{"type":"string"}},"required":["start_time","end_time"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"name":{"type":"string"},"time_intervals":{"items":{"$ref":"#/components/schemas/Interval"},"type":"array"}},"required":["name","time_intervals"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaTimeIntervalv0alpha1 = []byte(`{"Interval":{"additionalProperties":false,"properties":{"days_of_month":{"items":{"type":"string"},"type":"array"},"location":{"type":"string"},"months":{"items":{"type":"string"},"type":"array"},"times":{"items":{"$ref":"#/components/schemas/TimeRange"},"type":"array"},"weekdays":{"items":{"type":"string"},"type":"array"},"years":{"items":{"type":"string"},"type":"array"}},"type":"object"},"TimeInterval":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"TimeRange":{"additionalProperties":false,"properties":{"end_time":{"type":"string"},"start_time":{"type":"string"}},"required":["start_time","end_time"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"name":{"type":"string"},"time_intervals":{"items":{"$ref":"#/components/schemas/Interval"},"type":"array"}},"required":["name","time_intervals"],"type":"object"}}`) versionSchemaTimeIntervalv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaTimeIntervalv0alpha1, &versionSchemaTimeIntervalv0alpha1) ) diff --git a/apps/alerting/rules/definitions/alerting-manifest.yaml b/apps/alerting/rules/definitions/alerting-manifest.yaml index 836bd2c9a94..a8927dd1ea4 100644 --- a/apps/alerting/rules/definitions/alerting-manifest.yaml +++ b/apps/alerting/rules/definitions/alerting-manifest.yaml @@ -8,7 +8,16 @@ spec: preferredVersion: v0alpha1 versions: - kinds: - - conversion: false + - admission: + mutation: + operations: + - CREATE + - UPDATE + validation: + operations: + - CREATE + - UPDATE + conversion: false kind: AlertRule plural: AlertRules schemas: @@ -214,7 +223,16 @@ spec: - spec.panelRef.dashboardUID - spec.panelRef.panelID - spec.notificationSettings.receiver - - conversion: false + - admission: + mutation: + operations: + - CREATE + - UPDATE + validation: + operations: + - CREATE + - UPDATE + conversion: false kind: RecordingRule plural: RecordingRules schemas: diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index d148f4a9c87..f1ccc575706 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,8 +3,9 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/prometheus/common v0.67.1 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -49,7 +50,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -67,13 +67,13 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index fe5ba699151..a0e59cd5731 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -163,8 +163,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -177,20 +177,20 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/alerting/rules/kinds/alertRule.cue b/apps/alerting/rules/kinds/alertRule.cue index bbc374c244c..f3c7293cf76 100644 --- a/apps/alerting/rules/kinds/alertRule.cue +++ b/apps/alerting/rules/kinds/alertRule.cue @@ -13,6 +13,18 @@ alertRulev0alpha1: alertRuleKind & { schema: { spec: v0alpha1.AlertRuleSpec } + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } + mutation: { + operations: [ + "CREATE", + "UPDATE", + ] + } selectableFields: [ "spec.title", "spec.paused", diff --git a/apps/alerting/rules/kinds/recordingRule.cue b/apps/alerting/rules/kinds/recordingRule.cue index 3a6e14907ba..3a126afc0f2 100644 --- a/apps/alerting/rules/kinds/recordingRule.cue +++ b/apps/alerting/rules/kinds/recordingRule.cue @@ -13,6 +13,18 @@ recordingRulev0alpha1: recordingRuleKind & { schema: { spec: v0alpha1.RecordingRuleSpec } + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } + mutation: { + operations: [ + "CREATE", + "UPDATE", + ] + } selectableFields: [ "spec.title", "spec.paused", diff --git a/apps/alerting/rules/pkg/apis/alerting/v0alpha1/alertrule_ext.go b/apps/alerting/rules/pkg/apis/alerting/v0alpha1/alertrule_ext.go index da4b07b3df8..a705925fd5f 100644 --- a/apps/alerting/rules/pkg/apis/alerting/v0alpha1/alertrule_ext.go +++ b/apps/alerting/rules/pkg/apis/alerting/v0alpha1/alertrule_ext.go @@ -3,6 +3,7 @@ package v0alpha1 import ( "fmt" "slices" + "time" ) func (o *AlertRule) GetProvenanceStatus() string { @@ -48,4 +49,78 @@ func (s *AlertRuleSpec) ExecErrStateOrDefault() string { return s.ExecErrState } -// TODO: add duration clamping for the field types AlertRulePromDuration, AlertRulePromDurationWMillis, and the For and KeepFiringFor string pointers +func (d *AlertRulePromDuration) ToDuration() (time.Duration, error) { + return ToDuration(string(*d)) +} + +func (d *AlertRulePromDurationWMillis) ToDuration() (time.Duration, error) { + return ToDuration(string(*d)) +} + +func (d *AlertRulePromDuration) Clamp() error { + clampedDuration, err := ClampDuration(string(*d)) + if err != nil { + return err + } + *d = AlertRulePromDuration(clampedDuration) + return nil +} + +func (d *AlertRulePromDurationWMillis) Clamp() error { + clampedDuration, err := ClampDuration(string(*d)) + if err != nil { + return err + } + *d = AlertRulePromDurationWMillis(clampedDuration) + return nil +} + +func (spec *AlertRuleSpec) ClampDurations() error { + // clamp all duration fields + if err := spec.Trigger.Interval.Clamp(); err != nil { + return err + } + if spec.For != nil { + clamped, err := ClampDuration(*spec.For) + if err != nil { + return err + } + spec.For = &clamped + } + if spec.KeepFiringFor != nil { + clamped, err := ClampDuration(*spec.KeepFiringFor) + if err != nil { + return err + } + spec.KeepFiringFor = &clamped + } + if spec.NotificationSettings != nil { + if spec.NotificationSettings.GroupWait != nil { + if err := spec.NotificationSettings.GroupWait.Clamp(); err != nil { + return err + } + } + if spec.NotificationSettings.GroupInterval != nil { + if err := spec.NotificationSettings.GroupInterval.Clamp(); err != nil { + return err + } + } + if spec.NotificationSettings.RepeatInterval != nil { + if err := spec.NotificationSettings.RepeatInterval.Clamp(); err != nil { + return err + } + } + } + for k, expr := range spec.Expressions { + if expr.RelativeTimeRange != nil { + if err := expr.RelativeTimeRange.From.Clamp(); err != nil { + return err + } + if err := expr.RelativeTimeRange.To.Clamp(); err != nil { + return err + } + spec.Expressions[k] = expr + } + } + return nil +} diff --git a/apps/alerting/rules/pkg/apis/alerting/v0alpha1/ext.go b/apps/alerting/rules/pkg/apis/alerting/v0alpha1/ext.go index 7b87f8ee8e9..f4efc73e014 100644 --- a/apps/alerting/rules/pkg/apis/alerting/v0alpha1/ext.go +++ b/apps/alerting/rules/pkg/apis/alerting/v0alpha1/ext.go @@ -1,17 +1,50 @@ package v0alpha1 +import ( + "fmt" + "time" + + prom_model "github.com/prometheus/common/model" +) + const ( InternalPrefix = "grafana.com/" GroupLabelKey = InternalPrefix + "group" GroupIndexLabelKey = GroupLabelKey + "-index" ProvenanceStatusAnnotationKey = InternalPrefix + "provenance" + // Copy of the max title length used in legacy validation path + AlertRuleMaxTitleLength = 190 + // Annotation key used to store the folder UID on resources + FolderAnnotationKey = "grafana.app/folder" + FolderLabelKey = FolderAnnotationKey ) +// NOTE: This is a copy of the constants from the alertrule package to avoid circular imports. +// Keep in sync with pkg/services/ngalert/models/provisioning.go const ( - ProvenanceStatusNone = "" - ProvenanceStatusAPI = "api" + ProvenanceStatusNone = "" + ProvenanceStatusAPI = "api" + ProvenanceStatusFile = "file" + ProvenanceStatusConvertedPrometheus = "converted_prometheus" ) var ( - AcceptedProvenanceStatuses = []string{ProvenanceStatusNone, ProvenanceStatusAPI} + AcceptedProvenanceStatuses = []string{ProvenanceStatusNone, ProvenanceStatusAPI, ProvenanceStatusFile, ProvenanceStatusConvertedPrometheus} ) + +func ToDuration(s string) (time.Duration, error) { + promDuration, err := prom_model.ParseDuration(s) + if err != nil { + return 0, fmt.Errorf("invalid duration format: %w", err) + } + return time.Duration(promDuration), nil +} + +// Convert the string duration to the longest valid Prometheus duration format (e.g., "60s" -> "1m") +func ClampDuration(s string) (string, error) { + promDuration, err := prom_model.ParseDuration(s) + if err != nil { + return "", fmt.Errorf("invalid duration format: %w", err) + } + return promDuration.String(), nil +} diff --git a/apps/alerting/rules/pkg/apis/alerting/v0alpha1/recordingrule_ext.go b/apps/alerting/rules/pkg/apis/alerting/v0alpha1/recordingrule_ext.go index 0d40fc4c7b6..50bd606ef39 100644 --- a/apps/alerting/rules/pkg/apis/alerting/v0alpha1/recordingrule_ext.go +++ b/apps/alerting/rules/pkg/apis/alerting/v0alpha1/recordingrule_ext.go @@ -3,6 +3,7 @@ package v0alpha1 import ( "fmt" "slices" + "time" ) func (o *RecordingRule) GetProvenanceStatus() string { @@ -27,4 +28,47 @@ func (o *RecordingRule) SetProvenanceStatus(status string) (err error) { return } -// TODO: add duration clamping for the field types RecordingRulePromDurationWMillis and RecordingRulePromDuration +func (d *RecordingRulePromDuration) ToDuration() (time.Duration, error) { + return ToDuration(string(*d)) +} + +func (d *RecordingRulePromDurationWMillis) ToDuration() (time.Duration, error) { + return ToDuration(string(*d)) +} + +func (d *RecordingRulePromDuration) Clamp() error { + clampedDuration, err := ClampDuration(string(*d)) + if err != nil { + return err + } + *d = RecordingRulePromDuration(clampedDuration) + return nil +} + +func (d *RecordingRulePromDurationWMillis) Clamp() error { + clampedDuration, err := ClampDuration(string(*d)) + if err != nil { + return err + } + *d = RecordingRulePromDurationWMillis(clampedDuration) + return nil +} + +func (spec *RecordingRuleSpec) ClampDurations() error { + // clamp all duration fields + if err := spec.Trigger.Interval.Clamp(); err != nil { + return err + } + for k, expr := range spec.Expressions { + if expr.RelativeTimeRange != nil { + if err := expr.RelativeTimeRange.From.Clamp(); err != nil { + return err + } + if err := expr.RelativeTimeRange.To.Clamp(); err != nil { + return err + } + spec.Expressions[k] = expr + } + } + return nil +} diff --git a/apps/alerting/rules/pkg/apis/alerting_manifest.go b/apps/alerting/rules/pkg/apis/alerting_manifest.go index 5057a58c6e6..f81fb7d43fc 100644 --- a/apps/alerting/rules/pkg/apis/alerting_manifest.go +++ b/apps/alerting/rules/pkg/apis/alerting_manifest.go @@ -42,7 +42,21 @@ var appManifestData = app.ManifestData{ Plural: "AlertRules", Scope: "Namespaced", Conversion: false, - Schema: &versionSchemaAlertRulev0alpha1, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + Mutation: &app.MutationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaAlertRulev0alpha1, SelectableFields: []string{ "spec.title", "spec.paused", @@ -57,7 +71,21 @@ var appManifestData = app.ManifestData{ Plural: "RecordingRules", Scope: "Namespaced", Conversion: false, - Schema: &versionSchemaRecordingRulev0alpha1, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + Mutation: &app.MutationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaRecordingRulev0alpha1, SelectableFields: []string{ "spec.title", "spec.paused", diff --git a/apps/alerting/rules/pkg/app/alertrule/mutator.go b/apps/alerting/rules/pkg/app/alertrule/mutator.go new file mode 100644 index 00000000000..a127e0699c9 --- /dev/null +++ b/apps/alerting/rules/pkg/app/alertrule/mutator.go @@ -0,0 +1,45 @@ +package alertrule + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/simple" + v1 "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/config" +) + +func NewMutator(cfg config.RuntimeConfig) *simple.Mutator { + return &simple.Mutator{ + MutateFunc: func(ctx context.Context, req *app.AdmissionRequest) (*app.MutatingResponse, error) { + // Mutate folder label to match folder UID from annotation + r, ok := req.Object.(*v1.AlertRule) + if !ok || r == nil { + // Nothing to do or wrong type; no mutation + return nil, nil + } + + // Read folder UID from annotation + folderUID := "" + if r.Annotations != nil { + folderUID = r.Annotations[v1.FolderAnnotationKey] + } + + // Ensure labels map exists and set the folder label if folderUID is present + if folderUID != "" { + if r.Labels == nil { + r.Labels = make(map[string]string) + } + // Maintain folder metadata label for downstream systems (alertmanager grouping etc.) + r.Labels[v1.FolderLabelKey] = folderUID + } + + // clamp all duration fields + if err := r.Spec.ClampDurations(); err != nil { + return nil, err + } + + return &app.MutatingResponse{UpdatedObject: r}, nil + }, + } +} diff --git a/apps/alerting/rules/pkg/app/alertrule/validator.go b/apps/alerting/rules/pkg/app/alertrule/validator.go new file mode 100644 index 00000000000..636c1b4a323 --- /dev/null +++ b/apps/alerting/rules/pkg/app/alertrule/validator.go @@ -0,0 +1,122 @@ +package alertrule + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana-app-sdk/simple" + model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/config" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/util" + prom_model "github.com/prometheus/common/model" +) + +// validateGroupLabels now delegates to util.ValidateGroupLabels for shared logic. +func validateGroupLabels(r *model.AlertRule, oldObject resource.Object, action resource.AdmissionAction) error { + var oldLabels map[string]string + if oldObject != nil { + if oldRule, ok := oldObject.(*model.AlertRule); ok { + oldLabels = oldRule.Labels + } else { + return fmt.Errorf("old object is not of type *v0alpha1.AlertRule") + } + } + return util.ValidateGroupLabels(r.Labels, oldLabels, action) +} + +func NewValidator(cfg config.RuntimeConfig) *simple.Validator { + return &simple.Validator{ + ValidateFunc: func(ctx context.Context, req *app.AdmissionRequest) error { + // Cast to specific type + r, ok := req.Object.(*model.AlertRule) + if !ok { + return fmt.Errorf("object is not of type *v0alpha1.AlertRule") + } + // 1) Validate provenance status annotation + sourceProv := r.GetProvenanceStatus() + if !slices.Contains(model.AcceptedProvenanceStatuses, sourceProv) { + return fmt.Errorf("invalid provenance status: %s", sourceProv) + } + + // 2) Validate group labels rules + if err := validateGroupLabels(r, req.OldObject, req.Action); err != nil { + return err + } + + // 3) Validate folder is set and exists + // Read folder UID directly from annotations + folderUID := "" + if r.Annotations != nil { + folderUID = r.Annotations[model.FolderAnnotationKey] + } + if folderUID == "" { + return fmt.Errorf("folder is required") + } + if cfg.FolderValidator != nil { + ok, verr := cfg.FolderValidator(ctx, folderUID) + if verr != nil { + return fmt.Errorf("failed to validate folder: %w", verr) + } + if !ok { + return fmt.Errorf("folder does not exist: %s", folderUID) + } + } + + // 4) Validate notification settings receiver if provided + if r.Spec.NotificationSettings != nil && r.Spec.NotificationSettings.Receiver != "" && cfg.NotificationSettingsValidator != nil { + ok, nerr := cfg.NotificationSettingsValidator(ctx, r.Spec.NotificationSettings.Receiver) + if nerr != nil { + return fmt.Errorf("failed to validate notification settings: %w", nerr) + } + if !ok { + return fmt.Errorf("invalid notification receiver: %s", r.Spec.NotificationSettings.Receiver) + } + } + + // 5) Enforce max title length + if len(r.Spec.Title) > model.AlertRuleMaxTitleLength { + return fmt.Errorf("alert rule title is too long. Max length is %d", model.AlertRuleMaxTitleLength) + } + + // 6) Validate evaluation interval against base interval + if err := util.ValidateInterval(cfg.BaseEvaluationInterval, &r.Spec.Trigger.Interval); err != nil { + return err + } + + // 7) Disallow reserved/spec system label keys + if r.Spec.Labels != nil { + for key := range r.Spec.Labels { + if _, bad := cfg.ReservedLabelKeys[key]; bad { + return fmt.Errorf("label key is reserved and cannot be specified: %s", key) + } + } + } + + // 8) For and KeepFiringFor must be >= 0 if set + if r.Spec.For != nil { + d, err := prom_model.ParseDuration(*r.Spec.For) + if err != nil { + return fmt.Errorf("invalid 'for' duration: %w", err) + } + if time.Duration(d) < 0 { + return fmt.Errorf("'for' cannot be less than 0") + } + } + if r.Spec.KeepFiringFor != nil { + d, err := prom_model.ParseDuration(*r.Spec.KeepFiringFor) + if err != nil { + return fmt.Errorf("invalid 'keepFiringFor' duration: %w", err) + } + if time.Duration(d) < 0 { + return fmt.Errorf("'keepFiringFor' cannot be less than 0") + } + } + + return nil + }, + } +} diff --git a/apps/alerting/rules/pkg/app/app.go b/apps/alerting/rules/pkg/app/app.go index ef0ab1883cf..3c546239c81 100644 --- a/apps/alerting/rules/pkg/app/app.go +++ b/apps/alerting/rules/pkg/app/app.go @@ -6,16 +6,29 @@ import ( "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/operator" + "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" "github.com/grafana/grafana/apps/alerting/rules/pkg/apis" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/alertrule" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/config" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/recordingrule" ) func New(cfg app.Config) (app.App, error) { managedKinds := make([]simple.AppManagedKind, 0) + runtimeCfg, ok := cfg.SpecificConfig.(config.RuntimeConfig) + if !ok { + return nil, config.ErrInvalidRuntimeConfig + } for _, kinds := range apis.GetKinds() { for _, kind := range kinds { - managedKinds = append(managedKinds, simple.AppManagedKind{Kind: kind}) + managedKind := simple.AppManagedKind{ + Kind: kind, + Validator: buildKindValidator(kind, runtimeCfg), + Mutator: buildKindMutator(kind, runtimeCfg), + } + managedKinds = append(managedKinds, managedKind) } } @@ -44,3 +57,23 @@ func New(cfg app.Config) (app.App, error) { return a, nil } + +func buildKindValidator(kind resource.Kind, cfg config.RuntimeConfig) *simple.Validator { + switch kind.Kind() { + case "AlertRule": + return alertrule.NewValidator(cfg) + case "RecordingRule": + return recordingrule.NewValidator(cfg) + } + return nil +} + +func buildKindMutator(kind resource.Kind, cfg config.RuntimeConfig) *simple.Mutator { + switch kind.Kind() { + case "AlertRule": + return alertrule.NewMutator(cfg) + case "RecordingRule": + return recordingrule.NewMutator(cfg) + } + return nil +} diff --git a/apps/alerting/rules/pkg/app/app_test.go b/apps/alerting/rules/pkg/app/app_test.go new file mode 100644 index 00000000000..405e397f8cd --- /dev/null +++ b/apps/alerting/rules/pkg/app/app_test.go @@ -0,0 +1,175 @@ +package app_test + +import ( + "context" + "testing" + "time" + + appsdk "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + + v1 "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/alertrule" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/config" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/recordingrule" +) + +func makeDefaultRuntimeConfig() config.RuntimeConfig { + return config.RuntimeConfig{ + FolderValidator: func(ctx context.Context, folderUID string) (bool, error) { return folderUID == "f1", nil }, + BaseEvaluationInterval: 60 * time.Second, // seconds + ReservedLabelKeys: map[string]struct{}{"__reserved__": {}, "grafana_folder": {}}, + NotificationSettingsValidator: func(ctx context.Context, receiver string) (bool, error) { return receiver == "notif-ok", nil }, + } +} + +func TestAlertRuleValidation_Success(t *testing.T) { + r := &v1.AlertRule{} + r.SetGroupVersionKind(v1.AlertRuleKind().GroupVersionKind()) + r.Name = "uid-1" + r.Namespace = "ns1" + r.Annotations = map[string]string{v1.FolderAnnotationKey: "f1"} + r.Labels = map[string]string{} + r.Spec = v1.AlertRuleSpec{ + Title: "ok", + Trigger: v1.AlertRuleIntervalTrigger{Interval: v1.AlertRulePromDuration("60s")}, + Expressions: v1.AlertRuleExpressionMap{"A": v1.AlertRuleExpression{Model: map[string]any{"expr": "1"}, Source: boolPtr(true)}}, + NoDataState: v1.DefaultNoDataState, + ExecErrState: v1.DefaultExecErrState, + NotificationSettings: &v1.AlertRuleV0alpha1SpecNotificationSettings{Receiver: "notif-ok"}, + } + + req := &appsdk.AdmissionRequest{Action: resource.AdmissionActionCreate, Object: r} + validator := alertrule.NewValidator(makeDefaultRuntimeConfig()) + if err := validator.Validate(context.Background(), req); err != nil { + t.Fatalf("expected success, got error: %v", err) + } +} + +func TestAlertRuleValidation_Errors(t *testing.T) { + mk := func(mut func(r *v1.AlertRule)) error { + r := baseAlertRule() + mut(r) + return alertrule.NewValidator(makeDefaultRuntimeConfig()).Validate(context.Background(), &appsdk.AdmissionRequest{Action: resource.AdmissionActionCreate, Object: r}) + } + + if err := mk(func(r *v1.AlertRule) { r.Annotations = nil }); err == nil { + t.Errorf("want folder required error") + } + if err := mk(func(r *v1.AlertRule) { r.Annotations[v1.FolderAnnotationKey] = "bad" }); err == nil { + t.Errorf("want folder not exist error") + } + if err := mk(func(r *v1.AlertRule) { r.Spec.Trigger.Interval = v1.AlertRulePromDuration("30s") }); err == nil { + t.Errorf("want base interval multiple error") + } + if err := mk(func(r *v1.AlertRule) { + r.Spec.NotificationSettings = &v1.AlertRuleV0alpha1SpecNotificationSettings{Receiver: "bad"} + }); err == nil { + t.Errorf("want invalid receiver error") + } + if err := mk(func(r *v1.AlertRule) { r.Labels[v1.GroupLabelKey] = "grp" }); err == nil { + t.Errorf("want group set on create error") + } + if err := mk(func(r *v1.AlertRule) { r.Spec.For = strPtr("-10s") }); err == nil { + t.Errorf("want for>=0 error") + } + if err := mk(func(r *v1.AlertRule) { + if r.Spec.Labels == nil { + r.Spec.Labels = map[string]v1.AlertRuleTemplateString{} + } + r.Spec.Labels["__reserved__"] = v1.AlertRuleTemplateString("x") + }); err == nil { + t.Errorf("want reserved label key error") + } +} + +func baseAlertRule() *v1.AlertRule { + r := &v1.AlertRule{} + r.SetGroupVersionKind(v1.AlertRuleKind().GroupVersionKind()) + r.Name = "uid-1" + r.Namespace = "ns1" + r.Annotations = map[string]string{v1.FolderAnnotationKey: "f1"} + r.Labels = map[string]string{} + r.Spec = v1.AlertRuleSpec{ + Title: "ok", + Trigger: v1.AlertRuleIntervalTrigger{Interval: v1.AlertRulePromDuration("60s")}, + Expressions: v1.AlertRuleExpressionMap{"A": v1.AlertRuleExpression{Model: map[string]any{"expr": "1"}, Source: boolPtr(true)}}, + NoDataState: v1.DefaultNoDataState, + ExecErrState: v1.DefaultExecErrState, + } + return r +} + +func TestRecordingRuleValidation_Success(t *testing.T) { + r := &v1.RecordingRule{} + r.SetGroupVersionKind(v1.RecordingRuleKind().GroupVersionKind()) + r.Name = "uid-2" + r.Namespace = "ns1" + r.Annotations = map[string]string{v1.FolderAnnotationKey: "f1"} + r.Labels = map[string]string{} + r.Spec = v1.RecordingRuleSpec{ + Title: "ok", + Trigger: v1.RecordingRuleIntervalTrigger{Interval: v1.RecordingRulePromDuration("60s")}, + Expressions: v1.RecordingRuleExpressionMap{"A": v1.RecordingRuleExpression{Model: map[string]any{"expr": "1"}, Source: boolPtr(true)}}, + Metric: "test_metric", + TargetDatasourceUID: "ds1", + } + + req := &appsdk.AdmissionRequest{Action: resource.AdmissionActionCreate, Object: r} + validator := recordingrule.NewValidator(makeDefaultRuntimeConfig()) + if err := validator.Validate(context.Background(), req); err != nil { + t.Fatalf("expected success, got error: %v", err) + } +} + +func TestRecordingRuleValidation_Errors(t *testing.T) { + mk := func(mut func(r *v1.RecordingRule)) error { + r := baseRecordingRule() + mut(r) + return recordingrule.NewValidator(makeDefaultRuntimeConfig()).Validate(context.Background(), &appsdk.AdmissionRequest{Action: resource.AdmissionActionCreate, Object: r}) + } + + if err := mk(func(r *v1.RecordingRule) { r.Annotations = nil }); err == nil { + t.Errorf("want folder required error") + } + if err := mk(func(r *v1.RecordingRule) { r.Annotations[v1.FolderAnnotationKey] = "bad" }); err == nil { + t.Errorf("want folder not exist error") + } + if err := mk(func(r *v1.RecordingRule) { r.Spec.Trigger.Interval = v1.RecordingRulePromDuration("30s") }); err == nil { + t.Errorf("want base interval multiple error") + } + if err := mk(func(r *v1.RecordingRule) { r.Labels[v1.GroupLabelKey] = "grp" }); err == nil { + t.Errorf("want group set on create error") + } + if err := mk(func(r *v1.RecordingRule) { r.Spec.Metric = "" }); err == nil { + t.Errorf("want metric required error") + } + if err := mk(func(r *v1.RecordingRule) { + if r.Spec.Labels == nil { + r.Spec.Labels = map[string]v1.RecordingRuleTemplateString{} + } + r.Spec.Labels["__reserved__"] = v1.RecordingRuleTemplateString("x") + }); err == nil { + t.Errorf("want reserved label key error") + } +} + +func baseRecordingRule() *v1.RecordingRule { + r := &v1.RecordingRule{} + r.SetGroupVersionKind(v1.RecordingRuleKind().GroupVersionKind()) + r.Name = "uid-1" + r.Namespace = "ns1" + r.Annotations = map[string]string{v1.FolderAnnotationKey: "f1"} + r.Labels = map[string]string{} + r.Spec = v1.RecordingRuleSpec{ + Title: "ok", + Trigger: v1.RecordingRuleIntervalTrigger{Interval: v1.RecordingRulePromDuration("60s")}, + Expressions: v1.RecordingRuleExpressionMap{"A": v1.RecordingRuleExpression{Model: map[string]any{"expr": "1"}, Source: boolPtr(true)}}, + Metric: "test_metric", + TargetDatasourceUID: "ds1", + } + return r +} + +func boolPtr(b bool) *bool { return &b } +func strPtr(s string) *string { return &s } diff --git a/apps/alerting/rules/pkg/app/config/runtime.go b/apps/alerting/rules/pkg/app/config/runtime.go new file mode 100644 index 00000000000..60936925a15 --- /dev/null +++ b/apps/alerting/rules/pkg/app/config/runtime.go @@ -0,0 +1,22 @@ +package config + +import ( + "context" + "errors" + "time" +) + +var ( + ErrInvalidRuntimeConfig = errors.New("invalid runtime config provided to alerting/rules app") +) + +// RuntimeConfig holds configuration values needed at runtime by the alerting/rules app from the running Grafana instance. +type RuntimeConfig struct { + // function to check folder existence given its uid + FolderValidator func(ctx context.Context, folderUID string) (bool, error) + // base evaluation interval + BaseEvaluationInterval time.Duration + // set of strings which are illegal for label keys on rules + ReservedLabelKeys map[string]struct{} + NotificationSettingsValidator func(ctx context.Context, receiver string) (bool, error) +} diff --git a/apps/alerting/rules/pkg/app/recordingrule/mutator.go b/apps/alerting/rules/pkg/app/recordingrule/mutator.go new file mode 100644 index 00000000000..31378c553f0 --- /dev/null +++ b/apps/alerting/rules/pkg/app/recordingrule/mutator.go @@ -0,0 +1,37 @@ +package recordingrule + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/simple" + v1 "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/config" +) + +func NewMutator(cfg config.RuntimeConfig) *simple.Mutator { + return &simple.Mutator{ + MutateFunc: func(ctx context.Context, req *app.AdmissionRequest) (*app.MutatingResponse, error) { + r, ok := req.Object.(*v1.RecordingRule) + if !ok || r == nil { + return nil, nil + } + + folderUID := "" + if r.Annotations != nil { + folderUID = r.Annotations[v1.FolderAnnotationKey] + } + + if folderUID != "" { + if r.Labels == nil { + r.Labels = make(map[string]string) + } + r.Labels[v1.FolderLabelKey] = folderUID + } + if err := r.Spec.ClampDurations(); err != nil { + return nil, err + } + return &app.MutatingResponse{UpdatedObject: r}, nil + }, + } +} diff --git a/apps/alerting/rules/pkg/app/recordingrule/validator.go b/apps/alerting/rules/pkg/app/recordingrule/validator.go new file mode 100644 index 00000000000..ea9e8f207d6 --- /dev/null +++ b/apps/alerting/rules/pkg/app/recordingrule/validator.go @@ -0,0 +1,95 @@ +package recordingrule + +import ( + "context" + "fmt" + "slices" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana-app-sdk/simple" + model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/config" + "github.com/grafana/grafana/apps/alerting/rules/pkg/app/util" + prom_model "github.com/prometheus/common/model" +) + +// validateGroupLabels now delegates to util.ValidateGroupLabels for shared logic. +func validateGroupLabels(r *model.RecordingRule, oldObject resource.Object, action resource.AdmissionAction) error { + var oldLabels map[string]string + if oldObject != nil { + if oldRule, ok := oldObject.(*model.RecordingRule); ok { + oldLabels = oldRule.Labels + } else { + return fmt.Errorf("old object is not of type *v0alpha1.RecordingRule") + } + } + return util.ValidateGroupLabels(r.Labels, oldLabels, action) +} + +func NewValidator(cfg config.RuntimeConfig) *simple.Validator { + return &simple.Validator{ + ValidateFunc: func(ctx context.Context, req *app.AdmissionRequest) error { + // Cast to specific type + r, ok := req.Object.(*model.RecordingRule) + if !ok { + return fmt.Errorf("object is not of type *v0alpha1.RecordingRule") + } + + sourceProv := r.GetProvenanceStatus() + if !slices.Contains(model.AcceptedProvenanceStatuses, sourceProv) { + return fmt.Errorf("invalid provenance status: %s", sourceProv) + } + + if err := validateGroupLabels(r, req.OldObject, req.Action); err != nil { + return err + } + + folderUID := "" + if r.Annotations != nil { + folderUID = r.Annotations[model.FolderAnnotationKey] + } + if folderUID == "" { + return fmt.Errorf("folder is required") + } + if cfg.FolderValidator != nil { + ok, verr := cfg.FolderValidator(ctx, folderUID) + if verr != nil { + return fmt.Errorf("failed to validate folder: %w", verr) + } + if !ok { + return fmt.Errorf("folder does not exist: %s", folderUID) + } + } + + if len(r.Spec.Title) > model.AlertRuleMaxTitleLength { + return fmt.Errorf("recording rule title is too long. Max length is %d", model.AlertRuleMaxTitleLength) + } + + if err := util.ValidateInterval(cfg.BaseEvaluationInterval, &r.Spec.Trigger.Interval); err != nil { + return err + } + + if r.Spec.Labels != nil { + for key := range r.Spec.Labels { + if _, bad := cfg.ReservedLabelKeys[key]; bad { + return fmt.Errorf("label key is reserved and cannot be specified: %s", key) + } + } + } + + if r.Spec.Metric == "" { + return fmt.Errorf("metric must be specified") + } + metric := prom_model.LabelValue(r.Spec.Metric) + if !metric.IsValid() { + return fmt.Errorf("metric contains invalid characters") + } + if !prom_model.IsValidMetricName(metric) { // nolint:staticcheck + return fmt.Errorf("invalid metric name") + } + + return nil + }, + } +} diff --git a/apps/alerting/rules/pkg/app/util/group_validation.go b/apps/alerting/rules/pkg/app/util/group_validation.go new file mode 100644 index 00000000000..8624ef816e8 --- /dev/null +++ b/apps/alerting/rules/pkg/app/util/group_validation.go @@ -0,0 +1,59 @@ +package util + +import ( + "fmt" + "strconv" + + "github.com/grafana/grafana-app-sdk/resource" + model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" +) + +// ValidateGroupLabels enforces the cross-field rules for group-related labels. +// +// Rules enforced: +// - On create, group-related labels must not be set. +// - If one of group or group-index is set, the other must also be set. +// - group-index must be an integer. +// - On update, group/group-index can only be present if they were present on the old object. +// +// Pass current labels and, when available, the previous object's labels. The previous labels +// may be nil when there is no old object (e.g., create operations). +func ValidateGroupLabels(labels map[string]string, oldLabels map[string]string, action resource.AdmissionAction) error { + groupStr, groupExists := labels[model.GroupLabelKey] + groupIndexStr, groupIndexStrExists := labels[model.GroupIndexLabelKey] + + if groupExists || groupIndexStrExists { + if action == resource.AdmissionActionCreate { + return fmt.Errorf("cannot set group when creating a new rule") + } + if groupExists && !groupIndexStrExists { + return fmt.Errorf("%s must be set when %s is set", model.GroupIndexLabelKey, model.GroupLabelKey) + } + if groupIndexStrExists && !groupExists { + return fmt.Errorf("%s must be set when %s is set", model.GroupLabelKey, model.GroupIndexLabelKey) + } + // Disallow empty values when labels are present + if groupExists && groupStr == "" { + return fmt.Errorf("%s cannot be empty", model.GroupLabelKey) + } + if groupIndexStrExists && groupIndexStr == "" { + return fmt.Errorf("%s cannot be empty", model.GroupIndexLabelKey) + } + if _, err := strconv.Atoi(groupIndexStr); err != nil { + return fmt.Errorf("invalid %s: %w", model.GroupIndexLabelKey, err) + } + + // On updates, ensure that group and group-index are only set if the old object had them set + if oldLabels != nil { + _, oldGroupExists := oldLabels[model.GroupLabelKey] + _, oldGroupIndexExists := oldLabels[model.GroupIndexLabelKey] + if groupExists && !oldGroupExists { + return fmt.Errorf("cannot set group when updating un-grouped rule") + } + if groupIndexStrExists && !oldGroupIndexExists { + return fmt.Errorf("cannot set group-index when updating un-grouped rule") + } + } + } + return nil +} diff --git a/apps/alerting/rules/pkg/app/util/group_validation_test.go b/apps/alerting/rules/pkg/app/util/group_validation_test.go new file mode 100644 index 00000000000..c2b44980b93 --- /dev/null +++ b/apps/alerting/rules/pkg/app/util/group_validation_test.go @@ -0,0 +1,109 @@ +package util + +import ( + "testing" + + "github.com/grafana/grafana-app-sdk/resource" + model "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" +) + +func TestValidateGroupLabels(t *testing.T) { + group := model.GroupLabelKey + groupIdx := model.GroupIndexLabelKey + + tests := []struct { + name string + labels map[string]string + oldLabels map[string]string + action resource.AdmissionAction + wantErr bool + }{ + { + name: "update empty group value", + labels: map[string]string{group: "", groupIdx: "1"}, + action: resource.AdmissionActionUpdate, + wantErr: true, + }, + { + name: "update empty group-index value", + labels: map[string]string{group: "g1", groupIdx: ""}, + action: resource.AdmissionActionUpdate, + wantErr: true, + }, + { + name: "create empty group value disallowed", + labels: map[string]string{group: ""}, + action: resource.AdmissionActionCreate, + wantErr: true, + }, + { + name: "create no labels allowed", + labels: nil, + action: resource.AdmissionActionCreate, + wantErr: false, + }, + { + name: "create with group disallowed", + labels: map[string]string{group: "g1"}, + action: resource.AdmissionActionCreate, + wantErr: true, + }, + { + name: "create with group-index disallowed", + labels: map[string]string{groupIdx: "1"}, + action: resource.AdmissionActionCreate, + wantErr: true, + }, + { + name: "update missing paired index", + labels: map[string]string{group: "g1"}, + action: resource.AdmissionActionUpdate, + wantErr: true, + }, + { + name: "update missing paired group", + labels: map[string]string{groupIdx: "1"}, + action: resource.AdmissionActionUpdate, + wantErr: true, + }, + { + name: "update invalid index format", + labels: map[string]string{group: "g1", groupIdx: "x"}, + oldLabels: map[string]string{group: "g1", groupIdx: "0"}, + action: resource.AdmissionActionUpdate, + wantErr: true, + }, + { + name: "update cannot add group when previously ungrouped", + labels: map[string]string{group: "g1", groupIdx: "0"}, + oldLabels: map[string]string{}, + action: resource.AdmissionActionUpdate, + wantErr: true, + }, + { + name: "update allowed when previously grouped", + labels: map[string]string{group: "g1", groupIdx: "2"}, + oldLabels: map[string]string{group: "g1", groupIdx: "1"}, + action: resource.AdmissionActionUpdate, + wantErr: false, + }, + { + name: "update no labels remains allowed", + labels: nil, + action: resource.AdmissionActionUpdate, + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := ValidateGroupLabels(tc.labels, tc.oldLabels, tc.action) + if tc.wantErr && err == nil { + t.Fatalf("expected error, got nil") + } + if !tc.wantErr && err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} diff --git a/apps/alerting/rules/pkg/app/util/validator.go b/apps/alerting/rules/pkg/app/util/validator.go new file mode 100644 index 00000000000..5f09c61e1cc --- /dev/null +++ b/apps/alerting/rules/pkg/app/util/validator.go @@ -0,0 +1,27 @@ +package util + +import ( + "fmt" + "time" +) + +type DurationLike interface { + ToDuration() (time.Duration, error) +} + +func ValidateInterval(baseInterval time.Duration, d DurationLike) error { + interval, err := d.ToDuration() + if err != nil { + return fmt.Errorf("invalid trigger interval: %w", err) + } + // Ensure interval is positive and an integer multiple of BaseEvaluationInterval (if provided) + if interval <= 0 { + return fmt.Errorf("trigger interval must be greater than 0") + } + if baseInterval > 0 { + if (interval % baseInterval) != 0 { + return fmt.Errorf("trigger interval must be a multiple of base evaluation interval (%s)", baseInterval.String()) + } + } + return nil +} diff --git a/apps/annotation/Makefile b/apps/annotation/Makefile new file mode 100644 index 00000000000..230bfd4149a --- /dev/null +++ b/apps/annotation/Makefile @@ -0,0 +1,9 @@ +include ../sdk.mk + +.PHONY: generate # Run Grafana App SDK code generation +generate: install-app-sdk update-app-sdk + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --defencoding=none \ No newline at end of file diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod new file mode 100644 index 00000000000..351d54e1cbd --- /dev/null +++ b/apps/annotation/go.mod @@ -0,0 +1,93 @@ +module github.com/grafana/grafana/apps/annotation + +go 1.24.0 + +require ( + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 + k8s.io/apimachinery v0.34.1 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect + github.com/onsi/gomega v1.36.2 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum new file mode 100644 index 00000000000..a0e59cd5731 --- /dev/null +++ b/apps/annotation/go.sum @@ -0,0 +1,240 @@ +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/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/annotation/kinds/annotation.cue b/apps/annotation/kinds/annotation.cue new file mode 100644 index 00000000000..5df0fe9f12a --- /dev/null +++ b/apps/annotation/kinds/annotation.cue @@ -0,0 +1,17 @@ +package kinds + +annotationv0alpha1: { + kind: "Annotation" + pluralName: "Annotations" + schema: { + spec: { + text: string + time: int64 + timeEnd?: int64 + dashboardUID?: string + panelID?: int64 + tags?: [...string] + } + } +} + diff --git a/apps/annotation/kinds/cue.mod/module.cue b/apps/annotation/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..a7f0a62536f --- /dev/null +++ b/apps/annotation/kinds/cue.mod/module.cue @@ -0,0 +1,2 @@ +module: "github.com/grafana/grafana/apps/annotation/kinds" +language: version: "v0.8.2" diff --git a/apps/annotation/kinds/manifest.cue b/apps/annotation/kinds/manifest.cue new file mode 100644 index 00000000000..b41931fc0c2 --- /dev/null +++ b/apps/annotation/kinds/manifest.cue @@ -0,0 +1,21 @@ +package kinds + +manifest: { + appName: "annotation" + groupOverride: "annotation.grafana.app" + versions: { + "v0alpha1": v0alpha1 + } +} + +v0alpha1: { + kinds: [annotationv0alpha1] + codegen: { + ts: { + enabled: true + } + go: { + enabled: true + } + } +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_client_gen.go similarity index 54% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go rename to apps/annotation/pkg/apis/annotation/v0alpha1/annotation_client_gen.go index e7788e27a33..c3f9cc986cc 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go +++ b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_client_gen.go @@ -7,33 +7,33 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type PluginMetaClient struct { - client *resource.TypedClient[*PluginMeta, *PluginMetaList] +type AnnotationClient struct { + client *resource.TypedClient[*Annotation, *AnnotationList] } -func NewPluginMetaClient(client resource.Client) *PluginMetaClient { - return &PluginMetaClient{ - client: resource.NewTypedClient[*PluginMeta, *PluginMetaList](client, PluginMetaKind()), +func NewAnnotationClient(client resource.Client) *AnnotationClient { + return &AnnotationClient{ + client: resource.NewTypedClient[*Annotation, *AnnotationList](client, AnnotationKind()), } } -func NewPluginMetaClientFromGenerator(generator resource.ClientGenerator) (*PluginMetaClient, error) { - c, err := generator.ClientFor(PluginMetaKind()) +func NewAnnotationClientFromGenerator(generator resource.ClientGenerator) (*AnnotationClient, error) { + c, err := generator.ClientFor(AnnotationKind()) if err != nil { return nil, err } - return NewPluginMetaClient(c), nil + return NewAnnotationClient(c), nil } -func (c *PluginMetaClient) Get(ctx context.Context, identifier resource.Identifier) (*PluginMeta, error) { +func (c *AnnotationClient) Get(ctx context.Context, identifier resource.Identifier) (*Annotation, error) { return c.client.Get(ctx, identifier) } -func (c *PluginMetaClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) { +func (c *AnnotationClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*AnnotationList, error) { return c.client.List(ctx, namespace, opts) } -func (c *PluginMetaClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) { +func (c *AnnotationClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*AnnotationList, error) { resp, err := c.client.List(ctx, namespace, resource.ListOptions{ ResourceVersion: opts.ResourceVersion, Limit: opts.Limit, @@ -61,25 +61,25 @@ func (c *PluginMetaClient) ListAll(ctx context.Context, namespace string, opts r return resp, nil } -func (c *PluginMetaClient) Create(ctx context.Context, obj *PluginMeta, opts resource.CreateOptions) (*PluginMeta, error) { +func (c *AnnotationClient) Create(ctx context.Context, obj *Annotation, opts resource.CreateOptions) (*Annotation, error) { // Make sure apiVersion and kind are set obj.APIVersion = GroupVersion.Identifier() - obj.Kind = PluginMetaKind().Kind() + obj.Kind = AnnotationKind().Kind() return c.client.Create(ctx, obj, opts) } -func (c *PluginMetaClient) Update(ctx context.Context, obj *PluginMeta, opts resource.UpdateOptions) (*PluginMeta, error) { +func (c *AnnotationClient) Update(ctx context.Context, obj *Annotation, opts resource.UpdateOptions) (*Annotation, error) { return c.client.Update(ctx, obj, opts) } -func (c *PluginMetaClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*PluginMeta, error) { +func (c *AnnotationClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Annotation, error) { return c.client.Patch(ctx, identifier, req, opts) } -func (c *PluginMetaClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus PluginMetaStatus, opts resource.UpdateOptions) (*PluginMeta, error) { - return c.client.Update(ctx, &PluginMeta{ +func (c *AnnotationClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus AnnotationStatus, opts resource.UpdateOptions) (*Annotation, error) { + return c.client.Update(ctx, &Annotation{ TypeMeta: metav1.TypeMeta{ - Kind: PluginMetaKind().Kind(), + Kind: AnnotationKind().Kind(), APIVersion: GroupVersion.Identifier(), }, ObjectMeta: metav1.ObjectMeta{ @@ -94,6 +94,6 @@ func (c *PluginMetaClient) UpdateStatus(ctx context.Context, identifier resource }) } -func (c *PluginMetaClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { +func (c *AnnotationClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_codec_gen.go similarity index 64% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go rename to apps/annotation/pkg/apis/annotation/v0alpha1/annotation_codec_gen.go index 77fb6f918a2..3d754151d55 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go +++ b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_codec_gen.go @@ -11,18 +11,18 @@ import ( "github.com/grafana/grafana-app-sdk/resource" ) -// PluginMetaJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding -type PluginMetaJSONCodec struct{} +// AnnotationJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type AnnotationJSONCodec struct{} // Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` -func (*PluginMetaJSONCodec) Read(reader io.Reader, into resource.Object) error { +func (*AnnotationJSONCodec) Read(reader io.Reader, into resource.Object) error { return json.NewDecoder(reader).Decode(into) } // Write writes JSON-encoded bytes into `writer` marshaled from `from` -func (*PluginMetaJSONCodec) Write(writer io.Writer, from resource.Object) error { +func (*AnnotationJSONCodec) Write(writer io.Writer, from resource.Object) error { return json.NewEncoder(writer).Encode(from) } // Interface compliance checks -var _ resource.Codec = &PluginMetaJSONCodec{} +var _ resource.Codec = &AnnotationJSONCodec{} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_metadata_gen.go similarity index 85% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go rename to apps/annotation/pkg/apis/annotation/v0alpha1/annotation_metadata_gen.go index 7d3b3c9c6b8..d35800b9804 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go +++ b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_metadata_gen.go @@ -9,7 +9,7 @@ import ( // metadata contains embedded CommonMetadata and can be extended with custom string fields // TODO: use CommonMetadata instead of redefining here; currently needs to be defined here // without external reference as using the CommonMetadata reference breaks thema codegen. -type PluginMetaMetadata struct { +type AnnotationMetadata struct { UpdateTimestamp time.Time `json:"updateTimestamp"` CreatedBy string `json:"createdBy"` Uid string `json:"uid"` @@ -22,9 +22,9 @@ type PluginMetaMetadata struct { Labels map[string]string `json:"labels"` } -// NewPluginMetaMetadata creates a new PluginMetaMetadata object. -func NewPluginMetaMetadata() *PluginMetaMetadata { - return &PluginMetaMetadata{ +// NewAnnotationMetadata creates a new AnnotationMetadata object. +func NewAnnotationMetadata() *AnnotationMetadata { + return &AnnotationMetadata{ Finalizers: []string{}, Labels: map[string]string{}, } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_object_gen.go b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_object_gen.go similarity index 67% rename from apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_object_gen.go rename to apps/annotation/pkg/apis/annotation/v0alpha1/annotation_object_gen.go index 6ceaa603baa..db99bcffa08 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_object_gen.go +++ b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_object_gen.go @@ -15,22 +15,22 @@ import ( ) // +k8s:openapi-gen=true -type PluginInstall struct { +type Annotation struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ObjectMeta `json:"metadata" yaml:"metadata"` - // Spec is the spec of the PluginInstall - Spec PluginInstallSpec `json:"spec" yaml:"spec"` + // Spec is the spec of the Annotation + Spec AnnotationSpec `json:"spec" yaml:"spec"` - Status PluginInstallStatus `json:"status" yaml:"status"` + Status AnnotationStatus `json:"status" yaml:"status"` } -func (o *PluginInstall) GetSpec() any { +func (o *Annotation) GetSpec() any { return o.Spec } -func (o *PluginInstall) SetSpec(spec any) error { - cast, ok := spec.(PluginInstallSpec) +func (o *Annotation) SetSpec(spec any) error { + cast, ok := spec.(AnnotationSpec) if !ok { return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) } @@ -38,13 +38,13 @@ func (o *PluginInstall) SetSpec(spec any) error { return nil } -func (o *PluginInstall) GetSubresources() map[string]any { +func (o *Annotation) GetSubresources() map[string]any { return map[string]any{ "status": o.Status, } } -func (o *PluginInstall) GetSubresource(name string) (any, bool) { +func (o *Annotation) GetSubresource(name string) (any, bool) { switch name { case "status": return o.Status, true @@ -53,12 +53,12 @@ func (o *PluginInstall) GetSubresource(name string) (any, bool) { } } -func (o *PluginInstall) SetSubresource(name string, value any) error { +func (o *Annotation) SetSubresource(name string, value any) error { switch name { case "status": - cast, ok := value.(PluginInstallStatus) + cast, ok := value.(AnnotationStatus) if !ok { - return fmt.Errorf("cannot set status type %#v, not of type PluginInstallStatus", value) + return fmt.Errorf("cannot set status type %#v, not of type AnnotationStatus", value) } o.Status = cast return nil @@ -67,7 +67,7 @@ func (o *PluginInstall) SetSubresource(name string, value any) error { } } -func (o *PluginInstall) GetStaticMetadata() resource.StaticMetadata { +func (o *Annotation) GetStaticMetadata() resource.StaticMetadata { gvk := o.GroupVersionKind() return resource.StaticMetadata{ Name: o.ObjectMeta.Name, @@ -78,7 +78,7 @@ func (o *PluginInstall) GetStaticMetadata() resource.StaticMetadata { } } -func (o *PluginInstall) SetStaticMetadata(metadata resource.StaticMetadata) { +func (o *Annotation) SetStaticMetadata(metadata resource.StaticMetadata) { o.Name = metadata.Name o.Namespace = metadata.Namespace o.SetGroupVersionKind(schema.GroupVersionKind{ @@ -88,7 +88,7 @@ func (o *PluginInstall) SetStaticMetadata(metadata resource.StaticMetadata) { }) } -func (o *PluginInstall) GetCommonMetadata() resource.CommonMetadata { +func (o *Annotation) GetCommonMetadata() resource.CommonMetadata { dt := o.DeletionTimestamp var deletionTimestamp *time.Time if dt != nil { @@ -120,7 +120,7 @@ func (o *PluginInstall) GetCommonMetadata() resource.CommonMetadata { } } -func (o *PluginInstall) SetCommonMetadata(metadata resource.CommonMetadata) { +func (o *Annotation) SetCommonMetadata(metadata resource.CommonMetadata) { o.UID = types.UID(metadata.UID) o.ResourceVersion = metadata.ResourceVersion o.Generation = metadata.Generation @@ -165,7 +165,7 @@ func (o *PluginInstall) SetCommonMetadata(metadata resource.CommonMetadata) { } } -func (o *PluginInstall) GetCreatedBy() string { +func (o *Annotation) GetCreatedBy() string { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -173,7 +173,7 @@ func (o *PluginInstall) GetCreatedBy() string { return o.ObjectMeta.Annotations["grafana.com/createdBy"] } -func (o *PluginInstall) SetCreatedBy(createdBy string) { +func (o *Annotation) SetCreatedBy(createdBy string) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -181,7 +181,7 @@ func (o *PluginInstall) SetCreatedBy(createdBy string) { o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy } -func (o *PluginInstall) GetUpdateTimestamp() time.Time { +func (o *Annotation) GetUpdateTimestamp() time.Time { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -190,7 +190,7 @@ func (o *PluginInstall) GetUpdateTimestamp() time.Time { return parsed } -func (o *PluginInstall) SetUpdateTimestamp(updateTimestamp time.Time) { +func (o *Annotation) SetUpdateTimestamp(updateTimestamp time.Time) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -198,7 +198,7 @@ func (o *PluginInstall) SetUpdateTimestamp(updateTimestamp time.Time) { o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) } -func (o *PluginInstall) GetUpdatedBy() string { +func (o *Annotation) GetUpdatedBy() string { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -206,7 +206,7 @@ func (o *PluginInstall) GetUpdatedBy() string { return o.ObjectMeta.Annotations["grafana.com/updatedBy"] } -func (o *PluginInstall) SetUpdatedBy(updatedBy string) { +func (o *Annotation) SetUpdatedBy(updatedBy string) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -214,21 +214,21 @@ func (o *PluginInstall) SetUpdatedBy(updatedBy string) { o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy } -func (o *PluginInstall) Copy() resource.Object { +func (o *Annotation) Copy() resource.Object { return resource.CopyObject(o) } -func (o *PluginInstall) DeepCopyObject() runtime.Object { +func (o *Annotation) DeepCopyObject() runtime.Object { return o.Copy() } -func (o *PluginInstall) DeepCopy() *PluginInstall { - cpy := &PluginInstall{} +func (o *Annotation) DeepCopy() *Annotation { + cpy := &Annotation{} o.DeepCopyInto(cpy) return cpy } -func (o *PluginInstall) DeepCopyInto(dst *PluginInstall) { +func (o *Annotation) DeepCopyInto(dst *Annotation) { dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) @@ -237,34 +237,34 @@ func (o *PluginInstall) DeepCopyInto(dst *PluginInstall) { } // Interface compliance compile-time check -var _ resource.Object = &PluginInstall{} +var _ resource.Object = &Annotation{} // +k8s:openapi-gen=true -type PluginInstallList struct { +type AnnotationList struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ListMeta `json:"metadata" yaml:"metadata"` - Items []PluginInstall `json:"items" yaml:"items"` + Items []Annotation `json:"items" yaml:"items"` } -func (o *PluginInstallList) DeepCopyObject() runtime.Object { +func (o *AnnotationList) DeepCopyObject() runtime.Object { return o.Copy() } -func (o *PluginInstallList) Copy() resource.ListObject { - cpy := &PluginInstallList{ +func (o *AnnotationList) Copy() resource.ListObject { + cpy := &AnnotationList{ TypeMeta: o.TypeMeta, - Items: make([]PluginInstall, len(o.Items)), + Items: make([]Annotation, len(o.Items)), } o.ListMeta.DeepCopyInto(&cpy.ListMeta) for i := 0; i < len(o.Items); i++ { - if item, ok := o.Items[i].Copy().(*PluginInstall); ok { + if item, ok := o.Items[i].Copy().(*Annotation); ok { cpy.Items[i] = *item } } return cpy } -func (o *PluginInstallList) GetItems() []resource.Object { +func (o *AnnotationList) GetItems() []resource.Object { items := make([]resource.Object, len(o.Items)) for i := 0; i < len(o.Items); i++ { items[i] = &o.Items[i] @@ -272,48 +272,48 @@ func (o *PluginInstallList) GetItems() []resource.Object { return items } -func (o *PluginInstallList) SetItems(items []resource.Object) { - o.Items = make([]PluginInstall, len(items)) +func (o *AnnotationList) SetItems(items []resource.Object) { + o.Items = make([]Annotation, len(items)) for i := 0; i < len(items); i++ { - o.Items[i] = *items[i].(*PluginInstall) + o.Items[i] = *items[i].(*Annotation) } } -func (o *PluginInstallList) DeepCopy() *PluginInstallList { - cpy := &PluginInstallList{} +func (o *AnnotationList) DeepCopy() *AnnotationList { + cpy := &AnnotationList{} o.DeepCopyInto(cpy) return cpy } -func (o *PluginInstallList) DeepCopyInto(dst *PluginInstallList) { +func (o *AnnotationList) DeepCopyInto(dst *AnnotationList) { resource.CopyObjectInto(dst, o) } // Interface compliance compile-time check -var _ resource.ListObject = &PluginInstallList{} +var _ resource.ListObject = &AnnotationList{} // Copy methods for all subresource types // DeepCopy creates a full deep copy of Spec -func (s *PluginInstallSpec) DeepCopy() *PluginInstallSpec { - cpy := &PluginInstallSpec{} +func (s *AnnotationSpec) DeepCopy() *AnnotationSpec { + cpy := &AnnotationSpec{} s.DeepCopyInto(cpy) return cpy } // DeepCopyInto deep copies Spec into another Spec object -func (s *PluginInstallSpec) DeepCopyInto(dst *PluginInstallSpec) { +func (s *AnnotationSpec) DeepCopyInto(dst *AnnotationSpec) { resource.CopyObjectInto(dst, s) } -// DeepCopy creates a full deep copy of PluginInstallStatus -func (s *PluginInstallStatus) DeepCopy() *PluginInstallStatus { - cpy := &PluginInstallStatus{} +// DeepCopy creates a full deep copy of AnnotationStatus +func (s *AnnotationStatus) DeepCopy() *AnnotationStatus { + cpy := &AnnotationStatus{} s.DeepCopyInto(cpy) return cpy } -// DeepCopyInto deep copies PluginInstallStatus into another PluginInstallStatus object -func (s *PluginInstallStatus) DeepCopyInto(dst *PluginInstallStatus) { +// DeepCopyInto deep copies AnnotationStatus into another AnnotationStatus object +func (s *AnnotationStatus) DeepCopyInto(dst *AnnotationStatus) { resource.CopyObjectInto(dst, s) } diff --git a/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_schema_gen.go b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_schema_gen.go new file mode 100644 index 00000000000..3127e6a8954 --- /dev/null +++ b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaAnnotation = resource.NewSimpleSchema("annotation.grafana.app", "v0alpha1", &Annotation{}, &AnnotationList{}, resource.WithKind("Annotation"), + resource.WithPlural("annotations"), resource.WithScope(resource.NamespacedScope)) + kindAnnotation = resource.Kind{ + Schema: schemaAnnotation, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &AnnotationJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func AnnotationKind() resource.Kind { + return kindAnnotation +} + +// Schema returns a resource.SimpleSchema representation of Annotation +func AnnotationSchema() *resource.SimpleSchema { + return schemaAnnotation +} + +// Interface compliance checks +var _ resource.Schema = kindAnnotation diff --git a/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_spec_gen.go b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_spec_gen.go new file mode 100644 index 00000000000..f748c6b4253 --- /dev/null +++ b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_spec_gen.go @@ -0,0 +1,18 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type AnnotationSpec struct { + Text string `json:"text"` + Time int64 `json:"time"` + TimeEnd *int64 `json:"timeEnd,omitempty"` + DashboardUID *string `json:"dashboardUID,omitempty"` + PanelID *int64 `json:"panelID,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// NewAnnotationSpec creates a new AnnotationSpec object. +func NewAnnotationSpec() *AnnotationSpec { + return &AnnotationSpec{} +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_status_gen.go similarity index 60% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go rename to apps/annotation/pkg/apis/annotation/v0alpha1/annotation_status_gen.go index 60fa37dbb32..f681750af88 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go +++ b/apps/annotation/pkg/apis/annotation/v0alpha1/annotation_status_gen.go @@ -3,42 +3,42 @@ package v0alpha1 // +k8s:openapi-gen=true -type PluginMetastatusOperatorState struct { +type AnnotationstatusOperatorState struct { // lastEvaluation is the ResourceVersion last evaluated LastEvaluation string `json:"lastEvaluation"` // state describes the state of the lastEvaluation. // It is limited to three possible states for machine evaluation. - State PluginMetaStatusOperatorStateState `json:"state"` + State AnnotationStatusOperatorStateState `json:"state"` // descriptiveState is an optional more descriptive state field which has no requirements on format DescriptiveState *string `json:"descriptiveState,omitempty"` // details contains any extra information that is operator-specific Details map[string]interface{} `json:"details,omitempty"` } -// NewPluginMetastatusOperatorState creates a new PluginMetastatusOperatorState object. -func NewPluginMetastatusOperatorState() *PluginMetastatusOperatorState { - return &PluginMetastatusOperatorState{} +// NewAnnotationstatusOperatorState creates a new AnnotationstatusOperatorState object. +func NewAnnotationstatusOperatorState() *AnnotationstatusOperatorState { + return &AnnotationstatusOperatorState{} } // +k8s:openapi-gen=true -type PluginMetaStatus struct { +type AnnotationStatus struct { // operatorStates is a map of operator ID to operator state evaluations. // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]PluginMetastatusOperatorState `json:"operatorStates,omitempty"` + OperatorStates map[string]AnnotationstatusOperatorState `json:"operatorStates,omitempty"` // additionalFields is reserved for future use AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` } -// NewPluginMetaStatus creates a new PluginMetaStatus object. -func NewPluginMetaStatus() *PluginMetaStatus { - return &PluginMetaStatus{} +// NewAnnotationStatus creates a new AnnotationStatus object. +func NewAnnotationStatus() *AnnotationStatus { + return &AnnotationStatus{} } // +k8s:openapi-gen=true -type PluginMetaStatusOperatorStateState string +type AnnotationStatusOperatorStateState string const ( - PluginMetaStatusOperatorStateStateSuccess PluginMetaStatusOperatorStateState = "success" - PluginMetaStatusOperatorStateStateInProgress PluginMetaStatusOperatorStateState = "in_progress" - PluginMetaStatusOperatorStateStateFailed PluginMetaStatusOperatorStateState = "failed" + AnnotationStatusOperatorStateStateSuccess AnnotationStatusOperatorStateState = "success" + AnnotationStatusOperatorStateStateInProgress AnnotationStatusOperatorStateState = "in_progress" + AnnotationStatusOperatorStateStateFailed AnnotationStatusOperatorStateState = "failed" ) diff --git a/apps/annotation/pkg/apis/annotation/v0alpha1/constants.go b/apps/annotation/pkg/apis/annotation/v0alpha1/constants.go new file mode 100644 index 00000000000..e28dcb0720d --- /dev/null +++ b/apps/annotation/pkg/apis/annotation/v0alpha1/constants.go @@ -0,0 +1,18 @@ +package v0alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "annotation.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v0alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/annotation/pkg/apis/annotation_manifest.go b/apps/annotation/pkg/apis/annotation_manifest.go new file mode 100644 index 00000000000..5e7d6ae8b07 --- /dev/null +++ b/apps/annotation/pkg/apis/annotation_manifest.go @@ -0,0 +1,124 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + v0alpha1 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1" +) + +var ( + rawSchemaAnnotationv0alpha1 = []byte(`{"Annotation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"dashboardUID":{"type":"string"},"panelID":{"type":"integer"},"tags":{"items":{"type":"string"},"type":"array"},"text":{"type":"string"},"time":{"type":"integer"},"timeEnd":{"type":"integer"}},"required":["text","time"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaAnnotationv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaAnnotationv0alpha1, &versionSchemaAnnotationv0alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "annotation", + Group: "annotation.grafana.app", + PreferredVersion: "v0alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v0alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Annotation", + Plural: "Annotations", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaAnnotationv0alpha1, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + Schemas: map[string]spec.Schema{}, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("annotation") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "Annotation/v0alpha1": v0alpha1.AnnotationKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/annotation/pkg/app/app.go b/apps/annotation/pkg/app/app.go new file mode 100644 index 00000000000..d1665c0e776 --- /dev/null +++ b/apps/annotation/pkg/app/app.go @@ -0,0 +1,54 @@ +package app + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana-app-sdk/simple" + "k8s.io/apimachinery/pkg/runtime/schema" + + annotationv0alpha1 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1" +) + +func New(cfg app.Config) (app.App, error) { + simpleConfig := simple.AppConfig{ + Name: "annotation", + KubeConfig: cfg.KubeConfig, + InformerConfig: simple.AppInformerConfig{ + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.FromContext(ctx).Error("Informer processing error", "error", err) + }, + }, + }, + ManagedKinds: []simple.AppManagedKind{{ + Kind: annotationv0alpha1.AnnotationKind(), + }, + }, + } + + a, err := simple.NewApp(simpleConfig) + if err != nil { + return nil, err + } + + err = a.ValidateManifest(cfg.ManifestData) + if err != nil { + return nil, err + } + + return a, nil +} + +func GetKinds() map[schema.GroupVersion][]resource.Kind { + gv := schema.GroupVersion{ + Group: annotationv0alpha1.AnnotationKind().Group(), + Version: annotationv0alpha1.AnnotationKind().Version(), + } + return map[schema.GroupVersion][]resource.Kind{ + gv: {annotationv0alpha1.AnnotationKind()}, + } +} diff --git a/apps/annotation/plugin/src/generated/annotation/v0alpha1/annotation_object_gen.ts b/apps/annotation/plugin/src/generated/annotation/v0alpha1/annotation_object_gen.ts new file mode 100644 index 00000000000..2f7316bd74b --- /dev/null +++ b/apps/annotation/plugin/src/generated/annotation/v0alpha1/annotation_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Annotation { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/shorturl/plugin/src/generated/shorturl/v1alpha1/types.metadata.gen.ts b/apps/annotation/plugin/src/generated/annotation/v0alpha1/types.metadata.gen.ts similarity index 100% rename from apps/shorturl/plugin/src/generated/shorturl/v1alpha1/types.metadata.gen.ts rename to apps/annotation/plugin/src/generated/annotation/v0alpha1/types.metadata.gen.ts diff --git a/apps/annotation/plugin/src/generated/annotation/v0alpha1/types.spec.gen.ts b/apps/annotation/plugin/src/generated/annotation/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..e35814f387e --- /dev/null +++ b/apps/annotation/plugin/src/generated/annotation/v0alpha1/types.spec.gen.ts @@ -0,0 +1,16 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface Spec { + text: string; + time: number; + timeEnd?: number; + dashboardUID?: string; + panelID?: number; + tags?: string[]; +} + +export const defaultSpec = (): Spec => ({ + text: "", + time: 0, +}); + diff --git a/apps/annotation/plugin/src/generated/annotation/v0alpha1/types.status.gen.ts b/apps/annotation/plugin/src/generated/annotation/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/annotation/plugin/src/generated/annotation/v0alpha1/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index a8dc35dec8f..c9c1e623da6 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/correlations go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -67,13 +67,13 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index fe5ba699151..a0e59cd5731 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -163,8 +163,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -177,20 +177,20 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index a1025296b08..de0bc8b4614 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,13 +5,13 @@ go 1.25.3 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 - github.com/grafana/grafana-app-sdk v0.47.0 - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana-plugin-sdk-go v0.281.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 - golang.org/x/net v0.45.0 + golang.org/x/net v0.46.0 k8s.io/apimachinery v0.34.1 k8s.io/apiserver v0.34.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -107,17 +107,17 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect + golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.28.0 // indirect + golang.org/x/mod v0.29.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 35aadacf8f9..7a46e1764bb 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -85,10 +85,10 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-plugin-sdk-go v0.281.0 h1:V8dGyatzcOLQeivFhBV2JWMwTSZH/clDnpfKG9p3dTA= github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= @@ -280,20 +280,20 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -313,22 +313,22 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 6bad69b84a9..5bca05229db 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -154,6 +154,8 @@ FieldConfigSource: { defaults: FieldConfig // Overrides are the options applied to specific fields overriding the defaults. overrides: [...{ + // Describes config override rules created when interacting with Grafana. + "__systemRef"?: string matcher: MatcherConfig properties: [...DynamicConfigValue] }] @@ -247,7 +249,8 @@ MatcherConfig: { } Threshold: { - value: number + // Value null means -Infinity + value: number | null color: string } diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index f0278fc431c..426fb179fd8 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -117,6 +117,10 @@ DashboardLink: { // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu DashboardLinkPlacement: "inControlsMenu" +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +AnnotationQueryPlacement: "inControlsMenu" + // A topic is attached to DataFrame metadata in query results. // This specifies where the data should be used. DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") @@ -152,6 +156,8 @@ FieldConfigSource: { defaults: FieldConfig // Overrides are the options applied to specific fields overriding the defaults. overrides: [...{ + // Describes config override rules created when interacting with Grafana. + "__systemRef"?: string matcher: MatcherConfig properties: [...DynamicConfigValue] }] @@ -245,7 +251,8 @@ MatcherConfig: { } Threshold: { - value: number + // Value null means -Infinity + value: number | null color: string } @@ -436,6 +443,8 @@ AnnotationQuerySpec: { name: string builtIn?: bool | *false filter?: AnnotationPanelFilter + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: AnnotationQueryPlacement legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling. } @@ -452,6 +461,7 @@ QueryOptionsSpec: { interval?: string cacheTimeout?: string hideTimeOverride?: bool + timeCompare?: string } DataQueryKind: { @@ -468,7 +478,7 @@ DataQueryKind: { PanelQuerySpec: { query: DataQueryKind - refId: string + refId: string | *"A" hidden: bool } @@ -869,7 +879,7 @@ IntervalVariableSpec: { auto: bool | *false auto_min: string | *"" auto_count: int | *0 - refresh: VariableRefresh + refresh: "onTimeRangeChanged" label?: string hide: VariableHide skipUrlSync: bool | *false diff --git a/apps/dashboard/pkg/apis/dashboard/cuevalidator/validator.go b/apps/dashboard/pkg/apis/dashboard/cuevalidator/validator.go new file mode 100644 index 00000000000..38792ade026 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/cuevalidator/validator.go @@ -0,0 +1,29 @@ +package cuevalidator + +import ( + "sync" + + "cuelang.org/go/cue" + cuejson "cuelang.org/go/encoding/json" +) + +// Validator provides thread-safe CUE schema validation. +// +// CUE is not safe for concurrent use: https://github.com/cue-lang/cue/discussions/1205#discussioncomment-1189238 +// This validator uses a mutex to protect concurrent access to the underlying CUE validation. +type Validator struct { + schema cue.Value + mu sync.Mutex +} + +func NewValidator(schema cue.Value) *Validator { + return &Validator{ + schema: schema, + } +} + +func (v *Validator) Validate(data []byte) error { + v.mu.Lock() + defer v.mu.Unlock() + return cuejson.Validate(data, v.schema) +} diff --git a/apps/dashboard/pkg/apis/dashboard/types.go b/apps/dashboard/pkg/apis/dashboard/types.go index 29d6f8458e3..e204bee7f37 100644 --- a/apps/dashboard/pkg/apis/dashboard/types.go +++ b/apps/dashboard/pkg/apis/dashboard/types.go @@ -3,8 +3,9 @@ package dashboard // Information about how the requesting user can use a given dashboard type DashboardAccess struct { // Metadata fields - Slug string `json:"slug,omitempty"` - Url string `json:"url,omitempty"` + Slug string `json:"slug,omitempty"` + Url string `json:"url,omitempty"` + IsPublic bool `json:"isPublic"` // The permissions part CanSave bool `json:"canSave"` diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue index 526ddba3bfc..9222dad2bc7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -177,6 +177,9 @@ lineage: schemas: [{ // Set to 1 for the standard annotation query all dashboards have by default. builtIn?: number | *0 + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: #AnnotationQueryPlacement + // unless datasources have migrated to the target+mapping, // they just spread their query into the base object :( ... @@ -302,6 +305,10 @@ lineage: schemas: [{ // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") + // Annotation Query placement. Defines where the annotation query should be displayed. + // - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu + #AnnotationQueryPlacement: "inControlsMenu" @cuetsy(kind="type") + // Dashboard action type #ActionType: "fetch" | "infinity" @cuetsy(kind="type") @@ -670,6 +677,10 @@ lineage: schemas: [{ // Controls if the timeFrom or timeShift overrides are shown in the panel header hideTimeOverride?: bool + // Compare the current time range with a previous period + // For example "1d" to compare current period but shifted back 1 day + timeCompare?: string + // Dynamically load the panel libraryPanel?: #LibraryPanelRef diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/librarypanel.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/librarypanel.go new file mode 100644 index 00000000000..be4ea4d037c --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/librarypanel.go @@ -0,0 +1,91 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LibraryPanel struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Panel properties + Spec LibraryPanelSpec `json:"spec"` + + // Status will show errors + Status *LibraryPanelStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LibraryPanelList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LibraryPanel `json:"items"` +} + +// +k8s:deepcopy-gen=true +type LibraryPanelSpec struct { + // The panel type + Type string `json:"type"` + + // The panel type + PluginVersion string `json:"pluginVersion,omitempty"` + + // The title of the library panel + Title string `json:"title,omitempty"` + + // The title of the panel when displayed in the dashboard + PanelTitle string `json:"panelTitle,omitempty"` + + // Library panel description + Description string `json:"description,omitempty"` + + // The options schema depends on the panel type + Options common.Unstructured `json:"options"` + + // The fieldConfig schema depends on the panel type + FieldConfig common.Unstructured `json:"fieldConfig"` + + // The default datasource type + Datasource *data.DataSourceRef `json:"datasource,omitempty"` + + // The grid position + GridPos GridPos `json:"gridPos,omitempty"` + + // Whether the panel is transparent + Transparent bool `json:"transparent,omitempty"` + + // The links for the panel + Links []common.Unstructured `json:"links,omitempty"` + + // The datasource queries + // +listType=atomic + Targets []data.DataQuery `json:"targets,omitempty"` +} + +// +k8s:deepcopy-gen=true +type GridPos struct { + W int `json:"w"` + H int `json:"h"` + X int `json:"x"` + Y int `json:"y"` +} + +// +k8s:deepcopy-gen=true +type LibraryPanelStatus struct { + // Translation warnings (mostly things that were in SQL columns but not found in the saved body) + Warnings []string `json:"warnings,omitempty"` + + // The properties previously stored in SQL that are not included in this model + Missing common.Unstructured `json:"missing,omitempty"` +} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/register.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/register.go index 16e30b909b6..7b3934492fa 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/register.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/register.go @@ -92,8 +92,6 @@ func addKnownTypes(scheme *runtime.Scheme) error { &Dashboard{}, &DashboardList{}, &DashboardWithAccessInfo{}, - &DashboardVersionList{}, - &VersionsQueryOptions{}, &LibraryPanel{}, &LibraryPanelList{}, &metav1.PartialObjectMetadata{}, diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/types.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/types.go index 1589fc62c8a..d0b222ac7d2 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/types.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/types.go @@ -1,136 +1,5 @@ package v0alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" -) - -// +k8s:deepcopy-gen=true -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardVersionList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []DashboardVersionInfo `json:"items"` -} - -// +k8s:deepcopy-gen=true -type DashboardVersionInfo struct { - // The internal ID for this version (will be replaced with resourceVersion) - Version int `json:"version"` - - // If the dashboard came from a previous version, it is set here - ParentVersion int `json:"parentVersion,omitempty"` - - // The creation timestamp for this version - Created int64 `json:"created"` - - // The user who created this version - CreatedBy string `json:"createdBy,omitempty"` - - // Message passed while saving the version - Message string `json:"message,omitempty"` -} - -// +k8s:deepcopy-gen=true -// +k8s:conversion-gen:explicit-from=net/url.Values -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type VersionsQueryOptions struct { - metav1.TypeMeta `json:",inline"` - - // Path is the URL path - // +optional - Path string `json:"path,omitempty"` - - // +optional - Version int64 `json:"version,omitempty"` -} - -// +k8s:deepcopy-gen=true -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type LibraryPanel struct { - metav1.TypeMeta `json:",inline"` - // Standard object's metadata - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata - // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` - - // Panel properties - Spec LibraryPanelSpec `json:"spec"` - - // Status will show errors - Status *LibraryPanelStatus `json:"status,omitempty"` -} - -// +k8s:deepcopy-gen=true -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type LibraryPanelList struct { - metav1.TypeMeta `json:",inline"` - // +optional - metav1.ListMeta `json:"metadata,omitempty"` - - Items []LibraryPanel `json:"items"` -} - -// +k8s:deepcopy-gen=true -type LibraryPanelSpec struct { - // The panel type - Type string `json:"type"` - - // The panel type - PluginVersion string `json:"pluginVersion,omitempty"` - - // The title of the library panel - Title string `json:"title,omitempty"` - - // The title of the panel when displayed in the dashboard - PanelTitle string `json:"panelTitle,omitempty"` - - // Library panel description - Description string `json:"description,omitempty"` - - // The options schema depends on the panel type - Options common.Unstructured `json:"options"` - - // The fieldConfig schema depends on the panel type - FieldConfig common.Unstructured `json:"fieldConfig"` - - // The default datasource type - Datasource *data.DataSourceRef `json:"datasource,omitempty"` - - // The grid position - GridPos GridPos `json:"gridPos,omitempty"` - - // Whether the panel is transparent - Transparent bool `json:"transparent,omitempty"` - - // The links for the panel - Links []common.Unstructured `json:"links,omitempty"` - - // The datasource queries - // +listType=set - Targets []data.DataQuery `json:"targets,omitempty"` -} - -// +k8s:deepcopy-gen=true -type GridPos struct { - W int `json:"w"` - H int `json:"h"` - X int `json:"x"` - Y int `json:"y"` -} - -// +k8s:deepcopy-gen=true -type LibraryPanelStatus struct { - // Translation warnings (mostly things that were in SQL columns but not found in the saved body) - Warnings []string `json:"warnings,omitempty"` - - // The properties previously stored in SQL that are not included in this model - Missing common.Unstructured `json:"missing,omitempty"` -} - // This is like the legacy DTO where access and metadata are all returned in a single call // +k8s:deepcopy-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -143,8 +12,9 @@ type DashboardWithAccessInfo struct { // +k8s:deepcopy-gen=true type DashboardAccess struct { // Metadata fields - Slug string `json:"slug,omitempty"` - Url string `json:"url,omitempty"` + Slug string `json:"slug,omitempty"` + Url string `json:"url,omitempty"` + IsPublic bool `json:"isPublic"` // The permissions part CanSave bool `json:"canSave"` diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go index 6fdf1dd4515..7c5573f600a 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go @@ -7,13 +7,13 @@ import ( "strings" "sync" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/cuevalidator" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" "k8s.io/apimachinery/pkg/util/validation/field" "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/errors" - cuejson "cuelang.org/go/encoding/json" ) func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorList, field.ErrorList) { @@ -33,7 +33,7 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis }, schemaVersionError } - if err := cuejson.Validate(data, getCueSchema()); err != nil { + if err := getValidator().Validate(data); err != nil { errs := field.ErrorList{} for _, e := range errors.Errors(err) { @@ -71,20 +71,21 @@ func formatErrorPath(path []string) string { } var ( - compiledSchema cue.Value - getSchemaOnce sync.Once + validator *cuevalidator.Validator + getSchemaOnce sync.Once ) //go:embed dashboard_kind.cue var schemaSource string -func getCueSchema() cue.Value { +func getValidator() *cuevalidator.Validator { getSchemaOnce.Do(func() { cueCtx := cuecontext.New() - compiledSchema = cueCtx.CompileString(schemaSource).LookupPath( + compiledSchema := cueCtx.CompileString(schemaSource).LookupPath( cue.ParsePath("lineage.schemas[0].schema.spec"), ) + validator = cuevalidator.NewValidator(compiledSchema) }) - return compiledSchema + return validator } diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.conversion.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.conversion.go index b76e6b614ab..48869b1c0e8 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.conversion.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.conversion.go @@ -8,7 +8,6 @@ package v0alpha1 import ( - url "net/url" unsafe "unsafe" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" @@ -53,11 +52,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VersionsQueryOptions)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_url_Values_To_v0alpha1_VersionsQueryOptions(a.(*url.Values), b.(*VersionsQueryOptions), scope) - }); err != nil { - return err - } return nil } @@ -118,6 +112,7 @@ func Convert_dashboard_AnnotationPermission_To_v0alpha1_AnnotationPermission(in func autoConvert_v0alpha1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { out.Slug = in.Slug out.Url = in.Url + out.IsPublic = in.IsPublic out.CanSave = in.CanSave out.CanEdit = in.CanEdit out.CanAdmin = in.CanAdmin @@ -135,6 +130,7 @@ func Convert_v0alpha1_DashboardAccess_To_dashboard_DashboardAccess(in *Dashboard func autoConvert_dashboard_DashboardAccess_To_v0alpha1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { out.Slug = in.Slug out.Url = in.Url + out.IsPublic = in.IsPublic out.CanSave = in.CanSave out.CanEdit = in.CanEdit out.CanAdmin = in.CanAdmin @@ -148,28 +144,3 @@ func autoConvert_dashboard_DashboardAccess_To_v0alpha1_DashboardAccess(in *dashb func Convert_dashboard_DashboardAccess_To_v0alpha1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { return autoConvert_dashboard_DashboardAccess_To_v0alpha1_DashboardAccess(in, out, s) } - -func autoConvert_url_Values_To_v0alpha1_VersionsQueryOptions(in *url.Values, out *VersionsQueryOptions, s conversion.Scope) error { - // WARNING: Field TypeMeta does not have json tag, skipping. - - if values, ok := map[string][]string(*in)["path"]; ok && len(values) > 0 { - if err := runtime.Convert_Slice_string_To_string(&values, &out.Path, s); err != nil { - return err - } - } else { - out.Path = "" - } - if values, ok := map[string][]string(*in)["version"]; ok && len(values) > 0 { - if err := runtime.Convert_Slice_string_To_int64(&values, &out.Version, s); err != nil { - return err - } - } else { - out.Version = 0 - } - return nil -} - -// Convert_url_Values_To_v0alpha1_VersionsQueryOptions is an autogenerated conversion function. -func Convert_url_Values_To_v0alpha1_VersionsQueryOptions(in *url.Values, out *VersionsQueryOptions, s conversion.Scope) error { - return autoConvert_url_Values_To_v0alpha1_VersionsQueryOptions(in, out, s) -} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go index efdbdf07999..bb1a1216a36 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go @@ -98,53 +98,6 @@ func (in *DashboardHit) DeepCopy() *DashboardHit { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardVersionInfo) DeepCopyInto(out *DashboardVersionInfo) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardVersionInfo. -func (in *DashboardVersionInfo) DeepCopy() *DashboardVersionInfo { - if in == nil { - return nil - } - out := new(DashboardVersionInfo) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardVersionList) DeepCopyInto(out *DashboardVersionList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DashboardVersionInfo, len(*in)) - copy(*out, *in) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardVersionList. -func (in *DashboardVersionList) DeepCopy() *DashboardVersionList { - if in == nil { - return nil - } - out := new(DashboardVersionList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardVersionList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DashboardWithAccessInfo) DeepCopyInto(out *DashboardWithAccessInfo) { *out = *in @@ -454,28 +407,3 @@ func (in *TermFacet) DeepCopy() *TermFacet { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *VersionsQueryOptions) DeepCopyInto(out *VersionsQueryOptions) { - *out = *in - out.TypeMeta = in.TypeMeta - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionsQueryOptions. -func (in *VersionsQueryOptions) DeepCopy() *VersionsQueryOptions { - if in == nil { - return nil - } - out := new(VersionsQueryOptions) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *VersionsQueryOptions) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go index 63af5e26b7e..b4a69d0031d 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go @@ -26,8 +26,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardList": schema_pkg_apis_dashboard_v0alpha1_DashboardList(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v0alpha1_DashboardMetadata(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardStatus": schema_pkg_apis_dashboard_v0alpha1_DashboardStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionList(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardWithAccessInfo(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.FacetResult": schema_pkg_apis_dashboard_v0alpha1_FacetResult(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.GridPos": schema_pkg_apis_dashboard_v0alpha1_GridPos(ref), @@ -41,7 +39,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SortableField": schema_pkg_apis_dashboard_v0alpha1_SortableField(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SortableFields": schema_pkg_apis_dashboard_v0alpha1_SortableFields(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.TermFacet": schema_pkg_apis_dashboard_v0alpha1_TermFacet(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v0alpha1_VersionsQueryOptions(ref), "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": commonv0alpha1.Unstructured{}.OpenAPIDefinition(), } } @@ -173,6 +170,13 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardAccess(ref common.ReferenceCall Format: "", }, }, + "isPublic": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, "canSave": { SchemaProps: spec.SchemaProps{ Description: "The permissions part", @@ -215,7 +219,7 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardAccess(ref common.ReferenceCall }, }, }, - Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, + Required: []string{"isPublic", "canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, }, }, Dependencies: []string{ @@ -555,104 +559,6 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardStatus(ref common.ReferenceCall } } -func schema_pkg_apis_dashboard_v0alpha1_DashboardVersionInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "version": { - SchemaProps: spec.SchemaProps{ - Description: "The internal ID for this version (will be replaced with resourceVersion)", - Default: 0, - Type: []string{"integer"}, - Format: "int32", - }, - }, - "parentVersion": { - SchemaProps: spec.SchemaProps{ - Description: "If the dashboard came from a previous version, it is set here", - Type: []string{"integer"}, - Format: "int32", - }, - }, - "created": { - SchemaProps: spec.SchemaProps{ - Description: "The creation timestamp for this version", - Default: 0, - Type: []string{"integer"}, - Format: "int64", - }, - }, - "createdBy": { - SchemaProps: spec.SchemaProps{ - Description: "The user who created this version", - Type: []string{"string"}, - Format: "", - }, - }, - "message": { - SchemaProps: spec.SchemaProps{ - Description: "Message passed while saving the version", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"version", "created"}, - }, - }, - } -} - -func schema_pkg_apis_dashboard_v0alpha1_DashboardVersionList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardVersionInfo"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardVersionInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - func schema_pkg_apis_dashboard_v0alpha1_DashboardWithAccessInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -983,7 +889,7 @@ func schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref common.ReferenceCal "targets": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-list-type": "set", + "x-kubernetes-list-type": "atomic", }, }, SchemaProps: spec.SchemaProps{ @@ -1283,42 +1189,3 @@ func schema_pkg_apis_dashboard_v0alpha1_TermFacet(ref common.ReferenceCallback) }, } } - -func schema_pkg_apis_dashboard_v0alpha1_VersionsQueryOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "path": { - SchemaProps: spec.SchemaProps{ - Description: "Path is the URL path", - Type: []string{"string"}, - Format: "", - }, - }, - "version": { - SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int64", - }, - }, - }, - }, - }, - } -} diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue index 526ddba3bfc..9222dad2bc7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue @@ -177,6 +177,9 @@ lineage: schemas: [{ // Set to 1 for the standard annotation query all dashboards have by default. builtIn?: number | *0 + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: #AnnotationQueryPlacement + // unless datasources have migrated to the target+mapping, // they just spread their query into the base object :( ... @@ -302,6 +305,10 @@ lineage: schemas: [{ // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") + // Annotation Query placement. Defines where the annotation query should be displayed. + // - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu + #AnnotationQueryPlacement: "inControlsMenu" @cuetsy(kind="type") + // Dashboard action type #ActionType: "fetch" | "infinity" @cuetsy(kind="type") @@ -670,6 +677,10 @@ lineage: schemas: [{ // Controls if the timeFrom or timeShift overrides are shown in the panel header hideTimeOverride?: bool + // Compare the current time range with a previous period + // For example "1d" to compare current period but shifted back 1 day + timeCompare?: string + // Dynamically load the panel libraryPanel?: #LibraryPanelRef diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/types.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/types.go index ee4125d8f6a..285c3661980 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/types.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/types.go @@ -123,8 +123,9 @@ type DashboardWithAccessInfo struct { // +k8s:deepcopy-gen=true type DashboardAccess struct { // Metadata fields - Slug string `json:"slug,omitempty"` - Url string `json:"url,omitempty"` + Slug string `json:"slug,omitempty"` + Url string `json:"url,omitempty"` + IsPublic bool `json:"isPublic"` // The permissions part CanSave bool `json:"canSave"` diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go index 7020aa46b90..b48e38ffd7b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go @@ -12,8 +12,8 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/errors" - cuejson "cuelang.org/go/encoding/json" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/cuevalidator" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) @@ -34,7 +34,7 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis }, schemaVersionError } - if err := cuejson.Validate(data, getCueSchema()); err != nil { + if err := getValidator().Validate(data); err != nil { errs := field.ErrorList{} for _, e := range errors.Errors(err) { @@ -72,20 +72,21 @@ func formatErrorPath(path []string) string { } var ( - compiledSchema cue.Value - getSchemaOnce sync.Once + validator *cuevalidator.Validator + getSchemaOnce sync.Once ) //go:embed dashboard_kind.cue var schemaSource string -func getCueSchema() cue.Value { +func getValidator() *cuevalidator.Validator { getSchemaOnce.Do(func() { cueCtx := cuecontext.New() - compiledSchema = cueCtx.CompileString(schemaSource).LookupPath( + compiledSchema := cueCtx.CompileString(schemaSource).LookupPath( cue.ParsePath("lineage.schemas[0].schema.spec"), ) + validator = cuevalidator.NewValidator(compiledSchema) }) - return compiledSchema + return validator } diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.conversion.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.conversion.go index e3f1f46785b..b8f76f5deb9 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.conversion.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.conversion.go @@ -118,6 +118,7 @@ func Convert_dashboard_AnnotationPermission_To_v1beta1_AnnotationPermission(in * func autoConvert_v1beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { out.Slug = in.Slug out.Url = in.Url + out.IsPublic = in.IsPublic out.CanSave = in.CanSave out.CanEdit = in.CanEdit out.CanAdmin = in.CanAdmin @@ -135,6 +136,7 @@ func Convert_v1beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardA func autoConvert_dashboard_DashboardAccess_To_v1beta1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { out.Slug = in.Slug out.Url = in.Url + out.IsPublic = in.IsPublic out.CanSave = in.CanSave out.CanEdit = in.CanEdit out.CanAdmin = in.CanAdmin diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.openapi.go index be747be2ae3..3e2bdeab5f8 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/zz_generated.openapi.go @@ -165,6 +165,13 @@ func schema_pkg_apis_dashboard_v1beta1_DashboardAccess(ref common.ReferenceCallb Format: "", }, }, + "isPublic": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, "canSave": { SchemaProps: spec.SchemaProps{ Description: "The permissions part", @@ -207,7 +214,7 @@ func schema_pkg_apis_dashboard_v1beta1_DashboardAccess(ref common.ReferenceCallb }, }, }, - Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, + Required: []string{"isPublic", "canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, }, }, Dependencies: []string{ diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 602f639f81a..76e0921eb88 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -158,6 +158,8 @@ FieldConfigSource: { defaults: FieldConfig // Overrides are the options applied to specific fields overriding the defaults. overrides: [...{ + // Describes config override rules created when interacting with Grafana. + "__systemRef"?: string matcher: MatcherConfig properties: [...DynamicConfigValue] }] @@ -251,7 +253,8 @@ MatcherConfig: { } Threshold: { - value: number + // Value null means -Infinity + value: number | null color: string } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index bb737fb8ac3..37d9ae8897f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -551,8 +551,9 @@ const ( // +k8s:openapi-gen=true type DashboardThreshold struct { - Value float64 `json:"value"` - Color string `json:"color"` + // Value null means -Infinity + Value *float64 `json:"value"` + Color string `json:"color"` } // NewDashboardThreshold creates a new DashboardThreshold object. @@ -1064,9 +1065,11 @@ type DashboardAutoGridLayoutSpec struct { // NewDashboardAutoGridLayoutSpec creates a new DashboardAutoGridLayoutSpec object. func NewDashboardAutoGridLayoutSpec() *DashboardAutoGridLayoutSpec { return &DashboardAutoGridLayoutSpec{ - MaxColumnCount: (func(input float64) *float64 { return &input })(3), - FillScreen: (func(input bool) *bool { return &input })(false), - Items: []DashboardAutoGridLayoutItemKind{}, + MaxColumnCount: (func(input float64) *float64 { return &input })(3), + ColumnWidthMode: DashboardAutoGridLayoutSpecColumnWidthModeStandard, + RowHeightMode: DashboardAutoGridLayoutSpecRowHeightModeStandard, + FillScreen: (func(input bool) *bool { return &input })(false), + Items: []DashboardAutoGridLayoutItemKind{}, } } @@ -1863,6 +1866,8 @@ func NewDashboardSpec() *DashboardSpec { // +k8s:openapi-gen=true type DashboardV2alpha1FieldConfigSourceOverrides struct { + // Describes config override rules created when interacting with Grafana. + SystemRef *string `json:"__systemRef,omitempty"` Matcher DashboardMatcherConfig `json:"matcher"` Properties []DashboardDynamicConfigValue `json:"properties"` } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/types.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/types.go index d3599063b7a..c48e2d00602 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/types.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/types.go @@ -123,8 +123,9 @@ type DashboardWithAccessInfo struct { // +k8s:deepcopy-gen=true type DashboardAccess struct { // Metadata fields - Slug string `json:"slug,omitempty"` - Url string `json:"url,omitempty"` + Slug string `json:"slug,omitempty"` + Url string `json:"url,omitempty"` + IsPublic bool `json:"isPublic"` // The permissions part CanSave bool `json:"canSave"` diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/validation.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/validation.go index 7c61faa8924..ca9dcd3e514 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/validation.go @@ -12,7 +12,8 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/errors" - cuejson "cuelang.org/go/encoding/json" + + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/cuevalidator" ) func ValidateDashboardSpec(obj *Dashboard) field.ErrorList { @@ -26,7 +27,7 @@ func ValidateDashboardSpec(obj *Dashboard) field.ErrorList { // Custom validation for action query params and headers validateAndTrimActionArrays(obj) - if err := cuejson.Validate(data, getCueSchema()); err != nil { + if err := getValidator().Validate(data); err != nil { errs := field.ErrorList{} for _, e := range errors.Errors(err) { @@ -123,20 +124,21 @@ func formatErrorPath(path []string) string { } var ( - compiledSchema cue.Value - getSchemaOnce sync.Once + validator *cuevalidator.Validator + getSchemaOnce sync.Once ) //go:embed dashboard_spec.cue var schemaSource string -func getCueSchema() cue.Value { +func getValidator() *cuevalidator.Validator { getSchemaOnce.Do(func() { cueCtx := cuecontext.New() - compiledSchema = cueCtx.CompileString(schemaSource).LookupPath( + compiledSchema := cueCtx.CompileString(schemaSource).LookupPath( cue.ParsePath("DashboardSpec"), ) + validator = cuevalidator.NewValidator(compiledSchema) }) - return compiledSchema + return validator } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.conversion.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.conversion.go index fe4c7479194..0ac74fef54e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.conversion.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.conversion.go @@ -118,6 +118,7 @@ func Convert_dashboard_AnnotationPermission_To_v2alpha1_AnnotationPermission(in func autoConvert_v2alpha1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { out.Slug = in.Slug out.Url = in.Url + out.IsPublic = in.IsPublic out.CanSave = in.CanSave out.CanEdit = in.CanEdit out.CanAdmin = in.CanAdmin @@ -135,6 +136,7 @@ func Convert_v2alpha1_DashboardAccess_To_dashboard_DashboardAccess(in *Dashboard func autoConvert_dashboard_DashboardAccess_To_v2alpha1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { out.Slug = in.Slug out.Url = in.Url + out.IsPublic = in.IsPublic out.CanSave = in.CanSave out.CanEdit = in.CanEdit out.CanAdmin = in.CanAdmin diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 548a8c93268..a167aa6d36e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -265,6 +265,13 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref common.ReferenceCall Format: "", }, }, + "isPublic": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, "canSave": { SchemaProps: spec.SchemaProps{ Description: "The permissions part", @@ -307,7 +314,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref common.ReferenceCall }, }, }, - Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, + Required: []string{"isPublic", "canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, }, }, Dependencies: []string{ @@ -4423,9 +4430,9 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardThreshold(ref common.ReferenceC Properties: map[string]spec.Schema{ "value": { SchemaProps: spec.SchemaProps{ - Default: 0, - Type: []string{"number"}, - Format: "double", + Description: "Value null means -Infinity", + Type: []string{"number"}, + Format: "double", }, }, "color": { @@ -4670,6 +4677,13 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1FieldConfigSourceOverri SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ + "__systemRef": { + SchemaProps: spec.SchemaProps{ + Description: "Describes config override rules created when interacting with Grafana.", + Type: []string{"string"}, + Format: "", + }, + }, "matcher": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list index a0691b0d9f4..25c9bf58f1c 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list @@ -67,6 +67,7 @@ API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/ap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrArrayOfString,String API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrFloat64,Float64 API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrFloat64,String +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardV2alpha1FieldConfigSourceOverrides,SystemRef API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RangeMap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RegexMap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,SpecialValueMap diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index fb46afb5c76..d9df0dd3ee3 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -121,6 +121,10 @@ DashboardLink: { // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu DashboardLinkPlacement: "inControlsMenu" +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +AnnotationQueryPlacement: "inControlsMenu" + // A topic is attached to DataFrame metadata in query results. // This specifies where the data should be used. DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") @@ -156,6 +160,8 @@ FieldConfigSource: { defaults: FieldConfig // Overrides are the options applied to specific fields overriding the defaults. overrides: [...{ + // Describes config override rules created when interacting with Grafana. + "__systemRef"?: string matcher: MatcherConfig properties: [...DynamicConfigValue] }] @@ -249,7 +255,8 @@ MatcherConfig: { } Threshold: { - value: number + // Value null means -Infinity + value: number | null color: string } @@ -440,6 +447,8 @@ AnnotationQuerySpec: { name: string builtIn?: bool | *false filter?: AnnotationPanelFilter + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: AnnotationQueryPlacement legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling. } @@ -456,6 +465,7 @@ QueryOptionsSpec: { interval?: string cacheTimeout?: string hideTimeOverride?: bool + timeCompare?: string } DataQueryKind: { @@ -472,7 +482,7 @@ DataQueryKind: { PanelQuerySpec: { query: DataQueryKind - refId: string + refId: string | *"A" hidden: bool } @@ -873,7 +883,7 @@ IntervalVariableSpec: { auto: bool | *false auto_min: string | *"" auto_count: int | *0 - refresh: VariableRefresh + refresh: "onTimeRangeChanged" label?: string hide: VariableHide skipUrlSync: bool | *false diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index 2ac53a7600c..fc0a8fcf26f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -30,6 +30,8 @@ type DashboardAnnotationQuerySpec struct { Name string `json:"name"` BuiltIn *bool `json:"builtIn,omitempty"` Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"` + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + Placement *string `json:"placement,omitempty"` // Catch-all field for datasource-specific properties. Should not be available in as code tooling. LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"` } @@ -37,8 +39,9 @@ type DashboardAnnotationQuerySpec struct { // NewDashboardAnnotationQuerySpec creates a new DashboardAnnotationQuerySpec object. func NewDashboardAnnotationQuerySpec() *DashboardAnnotationQuerySpec { return &DashboardAnnotationQuerySpec{ - Query: *NewDashboardDataQueryKind(), - BuiltIn: (func(input bool) *bool { return &input })(false), + Query: *NewDashboardDataQueryKind(), + BuiltIn: (func(input bool) *bool { return &input })(false), + Placement: (func(input string) *string { return &input })(DashboardAnnotationQueryPlacement), } } @@ -78,6 +81,11 @@ func NewDashboardAnnotationPanelFilter() *DashboardAnnotationPanelFilter { } } +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +// +k8s:openapi-gen=true +const DashboardAnnotationQueryPlacement = "inControlsMenu" + // "Off" for no shared crosshair or tooltip (default). // "Crosshair" for shared crosshair. // "Tooltip" for shared crosshair AND shared tooltip. @@ -201,6 +209,7 @@ type DashboardPanelQuerySpec struct { func NewDashboardPanelQuerySpec() *DashboardPanelQuerySpec { return &DashboardPanelQuerySpec{ Query: *NewDashboardDataQueryKind(), + RefId: "A", } } @@ -278,6 +287,7 @@ type DashboardQueryOptionsSpec struct { Interval *string `json:"interval,omitempty"` CacheTimeout *string `json:"cacheTimeout,omitempty"` HideTimeOverride *bool `json:"hideTimeOverride,omitempty"` + TimeCompare *string `json:"timeCompare,omitempty"` } // NewDashboardQueryOptionsSpec creates a new DashboardQueryOptionsSpec object. @@ -545,8 +555,9 @@ const ( // +k8s:openapi-gen=true type DashboardThreshold struct { - Value float64 `json:"value"` - Color string `json:"color"` + // Value null means -Infinity + Value *float64 `json:"value"` + Color string `json:"color"` } // NewDashboardThreshold creates a new DashboardThreshold object. @@ -1058,9 +1069,11 @@ type DashboardAutoGridLayoutSpec struct { // NewDashboardAutoGridLayoutSpec creates a new DashboardAutoGridLayoutSpec object. func NewDashboardAutoGridLayoutSpec() *DashboardAutoGridLayoutSpec { return &DashboardAutoGridLayoutSpec{ - MaxColumnCount: (func(input float64) *float64 { return &input })(3), - FillScreen: (func(input bool) *bool { return &input })(false), - Items: []DashboardAutoGridLayoutItemKind{}, + MaxColumnCount: (func(input float64) *float64 { return &input })(3), + ColumnWidthMode: DashboardAutoGridLayoutSpecColumnWidthModeStandard, + RowHeightMode: DashboardAutoGridLayoutSpecRowHeightModeStandard, + FillScreen: (func(input bool) *bool { return &input })(false), + Items: []DashboardAutoGridLayoutItemKind{}, } } @@ -1604,7 +1617,7 @@ type DashboardIntervalVariableSpec struct { Auto bool `json:"auto"` AutoMin string `json:"auto_min"` AutoCount int64 `json:"auto_count"` - Refresh DashboardVariableRefresh `json:"refresh"` + Refresh string `json:"refresh"` Label *string `json:"label,omitempty"` Hide DashboardVariableHide `json:"hide"` SkipUrlSync bool `json:"skipUrlSync"` @@ -1628,7 +1641,7 @@ func NewDashboardIntervalVariableSpec() *DashboardIntervalVariableSpec { Auto: false, AutoMin: "", AutoCount: 0, - Refresh: DashboardVariableRefreshNever, + Refresh: "onTimeRangeChanged", Hide: DashboardVariableHideDontHide, SkipUrlSync: false, } @@ -1915,6 +1928,8 @@ func NewDashboardV2beta1DataQueryKindDatasource() *DashboardV2beta1DataQueryKind // +k8s:openapi-gen=true type DashboardV2beta1FieldConfigSourceOverrides struct { + // Describes config override rules created when interacting with Grafana. + SystemRef *string `json:"__systemRef,omitempty"` Matcher DashboardMatcherConfig `json:"matcher"` Properties []DashboardDynamicConfigValue `json:"properties"` } diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/types.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/types.go index 8ecda3cfd7d..07a0cf6da4f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/types.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/types.go @@ -123,8 +123,9 @@ type DashboardWithAccessInfo struct { // +k8s:deepcopy-gen=true type DashboardAccess struct { // Metadata fields - Slug string `json:"slug,omitempty"` - Url string `json:"url,omitempty"` + Slug string `json:"slug,omitempty"` + Url string `json:"url,omitempty"` + IsPublic bool `json:"isPublic"` // The permissions part CanSave bool `json:"canSave"` diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/validation.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/validation.go index 7c859626b98..518c133bcd7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/validation.go @@ -12,7 +12,8 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/errors" - cuejson "cuelang.org/go/encoding/json" + + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/cuevalidator" ) func ValidateDashboardSpec(obj *Dashboard) field.ErrorList { @@ -26,7 +27,7 @@ func ValidateDashboardSpec(obj *Dashboard) field.ErrorList { // Custom validation for action query params and headers validateAndTrimActionArrays(obj) - if err := cuejson.Validate(data, getCueSchema()); err != nil { + if err := getValidator().Validate(data); err != nil { errs := field.ErrorList{} for _, e := range errors.Errors(err) { @@ -123,20 +124,21 @@ func formatErrorPath(path []string) string { } var ( - compiledSchema cue.Value - getSchemaOnce sync.Once + validator *cuevalidator.Validator + getSchemaOnce sync.Once ) //go:embed dashboard_spec.cue var schemaSource string -func getCueSchema() cue.Value { +func getValidator() *cuevalidator.Validator { getSchemaOnce.Do(func() { cueCtx := cuecontext.New() - compiledSchema = cueCtx.CompileString(schemaSource).LookupPath( + compiledSchema := cueCtx.CompileString(schemaSource).LookupPath( cue.ParsePath("DashboardSpec"), ) + validator = cuevalidator.NewValidator(compiledSchema) }) - return compiledSchema + return validator } diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.conversion.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.conversion.go index fce43178b8a..9d7ab0b1ee2 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.conversion.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.conversion.go @@ -118,6 +118,7 @@ func Convert_dashboard_AnnotationPermission_To_v2beta1_AnnotationPermission(in * func autoConvert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { out.Slug = in.Slug out.Url = in.Url + out.IsPublic = in.IsPublic out.CanSave = in.CanSave out.CanEdit = in.CanEdit out.CanAdmin = in.CanAdmin @@ -135,6 +136,7 @@ func Convert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardA func autoConvert_dashboard_DashboardAccess_To_v2beta1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { out.Slug = in.Slug out.Url = in.Url + out.IsPublic = in.IsPublic out.CanSave = in.CanSave out.CanEdit = in.CanEdit out.CanAdmin = in.CanAdmin diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index 5e51134e80c..f833f1ddee0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -269,6 +269,13 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAccess(ref common.ReferenceCallb Format: "", }, }, + "isPublic": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, "canSave": { SchemaProps: spec.SchemaProps{ Description: "The permissions part", @@ -311,7 +318,7 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAccess(ref common.ReferenceCallb }, }, }, - Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, + Required: []string{"isPublic", "canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, }, }, Dependencies: []string{ @@ -760,6 +767,13 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref common.R Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter"), }, }, + "placement": { + SchemaProps: spec.SchemaProps{ + Description: "Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.", + Type: []string{"string"}, + Format: "", + }, + }, "legacyOptions": { SchemaProps: spec.SchemaProps{ Description: "Catch-all field for datasource-specific properties. Should not be available in as code tooling.", @@ -3426,6 +3440,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardQueryOptionsSpec(ref common.Refe Format: "", }, }, + "timeCompare": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, @@ -4529,9 +4549,9 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardThreshold(ref common.ReferenceCa Properties: map[string]spec.Schema{ "value": { SchemaProps: spec.SchemaProps{ - Default: 0, - Type: []string{"number"}, - Format: "double", + Description: "Value null means -Infinity", + Type: []string{"number"}, + Format: "double", }, }, "color": { @@ -4812,6 +4832,13 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardV2beta1FieldConfigSourceOverride SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ + "__systemRef": { + SchemaProps: spec.SchemaProps{ + Description: "Describes config override rules created when interacting with Grafana.", + Type: []string{"string"}, + Format: "", + }, + }, "matcher": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi_violation_exceptions.list index 5a0edf5747d..3007e07c806 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi_violation_exceptions.list @@ -68,6 +68,7 @@ API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/ap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardStringOrArrayOfString,String API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardStringOrFloat64,Float64 API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardStringOrFloat64,String +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardV2beta1FieldConfigSourceOverrides,SystemRef API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RangeMap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RegexMap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,SpecialValueMap diff --git a/apps/dashboard/pkg/migration/README.md b/apps/dashboard/pkg/migration/README.md index 4dffadf42bb..422038d6128 100644 --- a/apps/dashboard/pkg/migration/README.md +++ b/apps/dashboard/pkg/migration/README.md @@ -198,7 +198,7 @@ grafana_dashboard_migration_conversion_failure_total{ ### Error Types -The `error_type` label classifies failures into three categories: +The `error_type` label classifies failures into four categories: #### 1. `conversion_error` - General conversion failures not related to schema migration @@ -215,6 +215,12 @@ The `error_type` label classifies failures into three categories: - These are logged as warnings rather than errors - Indicates dashboards that cannot be migrated automatically +#### 4. `conversion_data_loss_error` +- Data loss detected during conversion +- Automatically checks that panels, queries, annotations, and links are preserved +- Triggered when target has fewer items than source +- Includes detailed loss metrics in logs (see [Data Loss Detection](#data-loss-detection)) + ### Logging #### Log structure @@ -236,6 +242,13 @@ All migration logs use structured logging with consistent field names: - `erroredConversionFunc` - Name of the conversion function that failed - `error` - The actual error message +**Data Loss Fields (conversion_data_loss_error only):** +- `panelsLost` - Number of panels lost +- `queriesLost` - Number of queries lost +- `annotationsLost` - Number of annotations lost +- `linksLost` - Number of links lost +- `variablesLost` - Number of template variables lost + #### Log levels ##### Success (DEBUG level) @@ -285,6 +298,55 @@ All migration logs use structured logging with consistent field names: } ``` +##### Data Loss Error (ERROR level) +```json +{ + "level": "error", + "msg": "Dashboard conversion failed", + "sourceVersionAPI": "dashboard.grafana.app/v1beta1", + "targetVersionAPI": "dashboard.grafana.app/v2alpha1", + "erroredConversionFunc": "V1beta1_to_V2alpha1", + "dashboardUID": "abc123", + "sourceSchemaVersion": 42, + "targetSchemaVersion": 42, + "panelsLost": 0, + "queriesLost": 2, + "annotationsLost": 0, + "linksLost": 0, + "variablesLost": 0, + "errorType": "conversion_data_loss_error", + "error": "data loss detected: query count decreased from 7 to 5" +} +``` + +### Data Loss Detection + +**Automatic Runtime Checks:** + +Every conversion automatically detects data loss by comparing: +- **Panel count** - Visualization panels (regular + library panels) +- **Query count** - Data source queries (excludes invalid row panel queries) +- **Annotation count** - Dashboard-level annotations +- **Link count** - Navigation links +- **Variable count** - Template variables (from `templating.list` in v0/v1, `variables` in v2) + +**Detection Logic:** +- ✅ **Allows additions**: Default annotations, enriched data +- ❌ **Detects losses**: Any decrease in counts triggers `conversion_data_loss_error` + +**Testing:** + +Run comprehensive data loss tests on all conversion test files: + +```bash +# Test all conversions for data loss +go test ./apps/dashboard/pkg/migration/conversion/... -run TestDataLossDetectionOnAllInputFiles -v + +# Test shows detailed panel/query analysis when loss is detected +``` + +**Implementation:** See `conversion/conversion_data_loss_detection.go` and `conversion/README.md` for details. + ### Implementation Details #### Automatic instrumentation @@ -293,6 +355,7 @@ All dashboard conversions are automatically instrumented via the `withConversion ```go // All conversion functions are wrapped automatically +// Includes metrics, logging, and data loss detection s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { return Convert_V0_to_V1(a.(*dashv0.Dashboard), b.(*dashv1.Dashboard), scope) @@ -319,6 +382,15 @@ type ConversionError struct { currentAPIVersion string targetAPIVersion string } + +// Data loss errors are detected when dashboard components (panels, queries, annotations, links, variables) +// are lost during conversion +type ConversionDataLossError struct { + functionName string // Function where data loss was detected (e.g., "V1_to_V2alpha1") + message string // Detailed error message with loss statistics + sourceAPIVersion string // Source API version (e.g., "dashboard.grafana.app/v1beta1") + targetAPIVersion string // Target API version (e.g., "dashboard.grafana.app/v2alpha1") +} ``` ### Registration diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index e38986c200c..90795b7779a 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -8,25 +8,26 @@ import ( dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) -func RegisterConversions(s *runtime.Scheme) error { +func RegisterConversions(s *runtime.Scheme, dsInfoProvider schemaversion.DataSourceInfoProvider) error { // v0 conversions if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V1(a.(*dashv0.Dashboard), b.(*dashv1.Dashboard), scope) + return Convert_V0_to_V1beta1(a.(*dashv0.Dashboard), b.(*dashv1.Dashboard), scope) })); err != nil { return err } if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv2alpha1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope) + return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsInfoProvider) })); err != nil { return err } if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv2beta1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope) + return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsInfoProvider) })); err != nil { return err } @@ -34,19 +35,19 @@ func RegisterConversions(s *runtime.Scheme) error { // v1 conversions if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv0.Dashboard)(nil), withConversionMetrics(dashv1.APIVERSION, dashv0.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1_to_V0(a.(*dashv1.Dashboard), b.(*dashv0.Dashboard), scope) + return Convert_V1beta1_to_V0(a.(*dashv1.Dashboard), b.(*dashv0.Dashboard), scope) })); err != nil { return err } if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), withConversionMetrics(dashv1.APIVERSION, dashv2alpha1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope) + return Convert_V1beta1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsInfoProvider) })); err != nil { return err } if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), withConversionMetrics(dashv1.APIVERSION, dashv2beta1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope) + return Convert_V1beta1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsInfoProvider) })); err != nil { return err } @@ -60,7 +61,7 @@ func RegisterConversions(s *runtime.Scheme) error { } if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv1.Dashboard)(nil), withConversionMetrics(dashv2alpha1.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2alpha1_to_V1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope) + return Convert_V2alpha1_to_V1beta1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope) })); err != nil { return err } @@ -80,7 +81,7 @@ func RegisterConversions(s *runtime.Scheme) error { } if err := s.AddConversionFunc((*dashv2beta1.Dashboard)(nil), (*dashv1.Dashboard)(nil), withConversionMetrics(dashv2beta1.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2beta1_to_V1(a.(*dashv2beta1.Dashboard), b.(*dashv1.Dashboard), scope) + return Convert_V2beta1_to_V1beta1(a.(*dashv2beta1.Dashboard), b.(*dashv1.Dashboard), scope) })); err != nil { return err } diff --git a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go new file mode 100644 index 00000000000..296a95ac054 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go @@ -0,0 +1,518 @@ +package conversion + +import ( + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/conversion" + + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" +) + +// Conversion Data Loss Detection Strategy for Dashboard Conversions +// +// This module implements comprehensive data loss detection to identify when data is lost during +// dashboard conversions between different API versions (v0alpha1, v1beta1, v2alpha1, v2beta1). +// +// # What We Detect +// +// The data loss detection system checks that critical dashboard components are preserved: +// +// 1. **Panel Count**: Total number of visualization panels (including library panels) +// - Counts regular panels and library panel references +// - Excludes row panels (which are layout containers, not visualizations) +// - Includes panels nested inside collapsed rows +// +// 2. **Query Count**: Total number of data source queries across all panels +// - Each panel can have multiple queries (targets) +// - Queries define what data is fetched and displayed +// - Loss of queries means loss of data visualization +// +// 3. **Annotation Count**: Number of annotation configurations +// - Annotations mark events and time ranges on visualizations +// - Each annotation can query a datasource for events +// +// 4. **Dashboard Link Count**: Number of navigation links to other dashboards or URLs +// - Links enable dashboard navigation workflows +// - Important for dashboard ecosystems + +// ConversionDataLossError represents data loss detected during dashboard conversion +type ConversionDataLossError struct { + functionName string + message string + sourceAPIVersion string + targetAPIVersion string +} + +// NewConversionDataLossError creates a new ConversionDataLossError +func NewConversionDataLossError(functionName, message, sourceAPIVersion, targetAPIVersion string) *ConversionDataLossError { + return &ConversionDataLossError{ + functionName: functionName, + message: message, + sourceAPIVersion: sourceAPIVersion, + targetAPIVersion: targetAPIVersion, + } +} + +// Error implements the error interface +func (e *ConversionDataLossError) Error() string { + return fmt.Sprintf("data loss detected in %s (%s → %s): %s", e.functionName, e.sourceAPIVersion, e.targetAPIVersion, e.message) +} + +// GetFunctionName returns the function name where data loss was detected +func (e *ConversionDataLossError) GetFunctionName() string { + return e.functionName +} + +// GetSourceAPIVersion returns the source API version +func (e *ConversionDataLossError) GetSourceAPIVersion() string { + return e.sourceAPIVersion +} + +// GetTargetAPIVersion returns the target API version +func (e *ConversionDataLossError) GetTargetAPIVersion() string { + return e.targetAPIVersion +} + +// dashboardStats contains statistics about a dashboard for data loss detection +type dashboardStats struct { + panelCount int + queryCount int + annotationCount int + linkCount int + variableCount int +} + +// countPanelsV0V1 counts panels in v0alpha1 or v1beta1 dashboard spec (unstructured JSON) +func countPanelsV0V1(spec map[string]interface{}) int { + if spec == nil { + return 0 + } + + panels, ok := spec["panels"].([]interface{}) + if !ok { + return 0 + } + + count := 0 + for _, p := range panels { + panelMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + + // Count regular panels (excluding row panels) + panelType, _ := panelMap["type"].(string) + if panelType != "row" { + count++ + } + + // Count collapsed panels inside row panels + if panelType == "row" { + if collapsedPanels, ok := panelMap["panels"].([]interface{}); ok { + count += len(collapsedPanels) + } + } + } + + return count +} + +// countQueriesV0V1 counts data queries in v0alpha1 or v1beta1 dashboard spec +// Note: Row panels are layout containers and should not have queries. +// We ignore any queries on row panels themselves, but count queries in their collapsed panels. +func countQueriesV0V1(spec map[string]interface{}) int { + if spec == nil { + return 0 + } + + panels, ok := spec["panels"].([]interface{}) + if !ok { + return 0 + } + + count := 0 + for _, p := range panels { + panelMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + + panelType, _ := panelMap["type"].(string) + + // Count queries in regular panels (NOT row panels) + if panelType != "row" { + if targets, ok := panelMap["targets"].([]interface{}); ok { + count += len(targets) + } + } + + // Count queries in collapsed panels inside row panels + if panelType == "row" { + if collapsedPanels, ok := panelMap["panels"].([]interface{}); ok { + for _, cp := range collapsedPanels { + if cpMap, ok := cp.(map[string]interface{}); ok { + if targets, ok := cpMap["targets"].([]interface{}); ok { + count += len(targets) + } + } + } + } + } + } + + return count +} + +// countAnnotationsV0V1 counts annotations in v0alpha1 or v1beta1 dashboard spec +func countAnnotationsV0V1(spec map[string]interface{}) int { + if spec == nil { + return 0 + } + + annotations, ok := spec["annotations"].(map[string]interface{}) + if !ok { + return 0 + } + + annotationList, ok := annotations["list"].([]interface{}) + if !ok { + return 0 + } + + return len(annotationList) +} + +// countLinksV0V1 counts dashboard links in v0alpha1 or v1beta1 dashboard spec +func countLinksV0V1(spec map[string]interface{}) int { + if spec == nil { + return 0 + } + + links, ok := spec["links"].([]interface{}) + if !ok { + return 0 + } + + return len(links) +} + +// countVariablesV0V1 counts template variables in v0alpha1 or v1beta1 dashboard spec +func countVariablesV0V1(spec map[string]interface{}) int { + if spec == nil { + return 0 + } + + templating, ok := spec["templating"].(map[string]interface{}) + if !ok { + return 0 + } + + variableList, ok := templating["list"].([]interface{}) + if !ok { + return 0 + } + + return len(variableList) +} + +// collectStatsV0V1 collects statistics from v0alpha1 or v1beta1 dashboard +func collectStatsV0V1(spec map[string]interface{}) dashboardStats { + return dashboardStats{ + panelCount: countPanelsV0V1(spec), + queryCount: countQueriesV0V1(spec), + annotationCount: countAnnotationsV0V1(spec), + linkCount: countLinksV0V1(spec), + variableCount: countVariablesV0V1(spec), + } +} + +// countPanelsV2 counts panels in v2alpha1 or v2beta1 dashboard spec (structured) +func countPanelsV2(elements map[string]dashv2alpha1.DashboardElement) int { + count := 0 + for _, element := range elements { + // Check if element is a Panel (not a LibraryPanel) + if element.PanelKind != nil { + count++ + } else if element.LibraryPanelKind != nil { + count++ + } + } + return count +} + +// countQueriesV2 counts data queries in v2alpha1 or v2beta1 dashboard spec +func countQueriesV2(elements map[string]dashv2alpha1.DashboardElement) int { + count := 0 + for _, element := range elements { + if element.PanelKind != nil { + count += len(element.PanelKind.Spec.Data.Spec.Queries) + } + } + return count +} + +// countAnnotationsV2 counts annotations in v2alpha1 or v2beta1 dashboard spec +func countAnnotationsV2(annotations []dashv2alpha1.DashboardAnnotationQueryKind) int { + return len(annotations) +} + +// countLinksV2 counts dashboard links in v2alpha1 or v2beta1 dashboard spec +func countLinksV2(links []dashv2alpha1.DashboardDashboardLink) int { + return len(links) +} + +// countVariablesV2 counts template variables in v2alpha1 or v2beta1 dashboard spec +func countVariablesV2(variables []dashv2alpha1.DashboardVariableKind) int { + return len(variables) +} + +// collectStatsV2alpha1 collects statistics from v2alpha1 dashboard +func collectStatsV2alpha1(spec dashv2alpha1.DashboardSpec) dashboardStats { + return dashboardStats{ + panelCount: countPanelsV2(spec.Elements), + queryCount: countQueriesV2(spec.Elements), + annotationCount: countAnnotationsV2(spec.Annotations), + linkCount: countLinksV2(spec.Links), + variableCount: countVariablesV2(spec.Variables), + } +} + +// countPanelsV2beta1 counts panels in v2beta1 dashboard spec +func countPanelsV2beta1(elements map[string]dashv2beta1.DashboardElement) int { + count := 0 + for _, element := range elements { + // Check if element is a Panel (not a LibraryPanel) + if element.PanelKind != nil { + count++ + } else if element.LibraryPanelKind != nil { + count++ + } + } + return count +} + +// countQueriesV2beta1 counts data queries in v2beta1 dashboard spec +func countQueriesV2beta1(elements map[string]dashv2beta1.DashboardElement) int { + count := 0 + for _, element := range elements { + if element.PanelKind != nil { + count += len(element.PanelKind.Spec.Data.Spec.Queries) + } + } + return count +} + +// countAnnotationsV2beta1 counts annotations in v2beta1 dashboard spec +func countAnnotationsV2beta1(annotations []dashv2beta1.DashboardAnnotationQueryKind) int { + return len(annotations) +} + +// countLinksV2beta1 counts dashboard links in v2beta1 dashboard spec +func countLinksV2beta1(links []dashv2beta1.DashboardDashboardLink) int { + return len(links) +} + +// countVariablesV2beta1 counts template variables in v2beta1 dashboard spec +func countVariablesV2beta1(variables []dashv2beta1.DashboardVariableKind) int { + return len(variables) +} + +// collectStatsV2beta1 collects statistics from v2beta1 dashboard +func collectStatsV2beta1(spec dashv2beta1.DashboardSpec) dashboardStats { + return dashboardStats{ + panelCount: countPanelsV2beta1(spec.Elements), + queryCount: countQueriesV2beta1(spec.Elements), + annotationCount: countAnnotationsV2beta1(spec.Annotations), + linkCount: countLinksV2beta1(spec.Links), + variableCount: countVariablesV2beta1(spec.Variables), + } +} + +// detectConversionDataLoss detects if critical dashboard data was lost during conversion +// Note: We only check for DATA LOSS (target < source), not additions (target > source). +// Conversions may add default values (like built-in annotations) which is expected behavior. +func detectConversionDataLoss(sourceStats, targetStats dashboardStats, sourceFuncName, targetFuncName string) error { + var errors []string + + // Panel count: detect loss only (target < source) + if targetStats.panelCount < sourceStats.panelCount { + errors = append(errors, fmt.Sprintf( + "panel count decreased: source=%d, target=%d (loss of %d panels)", + sourceStats.panelCount, + targetStats.panelCount, + sourceStats.panelCount-targetStats.panelCount, + )) + } + + // Query count: detect loss only (target < source) + if targetStats.queryCount < sourceStats.queryCount { + errors = append(errors, fmt.Sprintf( + "query count decreased: source=%d, target=%d (loss of %d queries)", + sourceStats.queryCount, + targetStats.queryCount, + sourceStats.queryCount-targetStats.queryCount, + )) + } + + // Annotation count: detect loss only (target < source) + // Note: Conversions may add default annotations, so additions are allowed + if targetStats.annotationCount < sourceStats.annotationCount { + errors = append(errors, fmt.Sprintf( + "annotation count decreased: source=%d, target=%d (loss of %d annotations)", + sourceStats.annotationCount, + targetStats.annotationCount, + sourceStats.annotationCount-targetStats.annotationCount, + )) + } + + // Dashboard link count: detect loss only (target < source) + if targetStats.linkCount < sourceStats.linkCount { + errors = append(errors, fmt.Sprintf( + "dashboard link count decreased: source=%d, target=%d (loss of %d links)", + sourceStats.linkCount, + targetStats.linkCount, + sourceStats.linkCount-targetStats.linkCount, + )) + } + + // Variable count: detect loss only (target < source) + if targetStats.variableCount < sourceStats.variableCount { + errors = append(errors, fmt.Sprintf( + "variable count decreased: source=%d, target=%d (loss of %d variables)", + sourceStats.variableCount, + targetStats.variableCount, + sourceStats.variableCount-targetStats.variableCount, + )) + } + + if len(errors) > 0 { + errorMsg := fmt.Sprintf("%v", errors) + // Note: sourceAPIVersion and targetAPIVersion are passed from checkConversionDataLoss + // For now, use empty strings - they will be set by the caller + return NewConversionDataLossError(fmt.Sprintf("%s_to_%s", sourceFuncName, targetFuncName), errorMsg, "", "") + } + + return nil +} + +// checkConversionDataLoss is the data loss check function that can be passed to withConversionMetrics +// It collects statistics from source and target dashboards and detects if data was lost +// Returns a ConversionDataLossError if data loss is detected, nil otherwise +func checkConversionDataLoss(sourceVersionAPI, targetVersionAPI string, a, b interface{}) error { + // Collect source statistics + sourceStats := collectDashboardStats(a) + + // Collect target statistics + targetStats := collectDashboardStats(b) + + // Detect if data was lost + err := detectConversionDataLoss(sourceStats, targetStats, convertAPIVersionToFuncName(sourceVersionAPI), convertAPIVersionToFuncName(targetVersionAPI)) + + // If data loss was detected, update the error with API versions + if err != nil { + var dataLossErr *ConversionDataLossError + if errors.As(err, &dataLossErr) { + dataLossErr.sourceAPIVersion = sourceVersionAPI + dataLossErr.targetAPIVersion = targetVersionAPI + } + } + + return err +} + +// collectDashboardStats collects statistics from a dashboard object (any version) +func collectDashboardStats(dashboard interface{}) dashboardStats { + switch d := dashboard.(type) { + case *dashv0.Dashboard: + if d.Spec.Object != nil { + return collectStatsV0V1(d.Spec.Object) + } + case *dashv1.Dashboard: + if d.Spec.Object != nil { + return collectStatsV0V1(d.Spec.Object) + } + case *dashv2alpha1.Dashboard: + return collectStatsV2alpha1(d.Spec) + case *dashv2beta1.Dashboard: + return collectStatsV2beta1(d.Spec) + } + return dashboardStats{} +} + +// withConversionDataLossDetection wraps a conversion function to detect data loss +func withConversionDataLossDetection(sourceFuncName, targetFuncName string, conversionFunc func(a, b interface{}, scope conversion.Scope) error) func(a, b interface{}, scope conversion.Scope) error { + return func(a, b interface{}, scope conversion.Scope) error { + // Collect source statistics + var sourceStats dashboardStats + switch source := a.(type) { + case *dashv0.Dashboard: + if source.Spec.Object != nil { + sourceStats = collectStatsV0V1(source.Spec.Object) + } + case *dashv1.Dashboard: + if source.Spec.Object != nil { + sourceStats = collectStatsV0V1(source.Spec.Object) + } + case *dashv2alpha1.Dashboard: + sourceStats = collectStatsV2alpha1(source.Spec) + case *dashv2beta1.Dashboard: + sourceStats = collectStatsV2beta1(source.Spec) + } + + // Execute the conversion + err := conversionFunc(a, b, scope) + if err != nil { + return err + } + + // Collect target statistics + var targetStats dashboardStats + switch target := b.(type) { + case *dashv0.Dashboard: + if target.Spec.Object != nil { + targetStats = collectStatsV0V1(target.Spec.Object) + } + case *dashv1.Dashboard: + if target.Spec.Object != nil { + targetStats = collectStatsV0V1(target.Spec.Object) + } + case *dashv2alpha1.Dashboard: + targetStats = collectStatsV2alpha1(target.Spec) + case *dashv2beta1.Dashboard: + targetStats = collectStatsV2beta1(target.Spec) + } + + // Detect if data was lost + if dataLossErr := detectConversionDataLoss(sourceStats, targetStats, sourceFuncName, targetFuncName); dataLossErr != nil { + logger.Error("Dashboard conversion data loss detected", + "sourceFunc", sourceFuncName, + "targetFunc", targetFuncName, + "sourcePanels", sourceStats.panelCount, + "targetPanels", targetStats.panelCount, + "sourceQueries", sourceStats.queryCount, + "targetQueries", targetStats.queryCount, + "sourceAnnotations", sourceStats.annotationCount, + "targetAnnotations", targetStats.annotationCount, + "sourceLinks", sourceStats.linkCount, + "targetLinks", targetStats.linkCount, + "error", dataLossErr, + ) + return dataLossErr + } + + logger.Debug("Dashboard conversion completed without data loss", + "sourceFunc", sourceFuncName, + "targetFunc", targetFuncName, + "panels", targetStats.panelCount, + "queries", targetStats.queryCount, + "annotations", targetStats.annotationCount, + "links", targetStats.linkCount, + ) + + return nil + } +} diff --git a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go new file mode 100644 index 00000000000..11bc3931990 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go @@ -0,0 +1,1360 @@ +package conversion + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/grafana/grafana/apps/dashboard/pkg/apis" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" +) + +func TestCountPanelsV0V1(t *testing.T) { + tests := []struct { + name string + spec map[string]interface{} + expected int + }{ + { + name: "nil spec", + spec: nil, + expected: 0, + }, + { + name: "empty spec", + spec: map[string]interface{}{}, + expected: 0, + }, + { + name: "dashboard with 2 regular panels", + spec: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + }, + map[string]interface{}{ + "id": float64(2), + "type": "table", + }, + }, + }, + expected: 2, + }, + { + name: "dashboard with row panel and collapsed panels", + spec: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + }, + map[string]interface{}{ + "id": float64(2), + "type": "row", + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(3), + "type": "graph", + }, + map[string]interface{}{ + "id": float64(4), + "type": "table", + }, + }, + }, + }, + }, + expected: 3, // 1 regular panel + 2 collapsed panels (row itself is not counted) + }, + { + name: "dashboard with only row panel (no collapsed panels)", + spec: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "row", + }, + }, + }, + expected: 0, // Row panels themselves are not counted + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count := countPanelsV0V1(tt.spec) + assert.Equal(t, tt.expected, count) + }) + } +} + +func TestCountQueriesV0V1(t *testing.T) { + tests := []struct { + name string + spec map[string]interface{} + expected int + }{ + { + name: "nil spec", + spec: nil, + expected: 0, + }, + { + name: "panel with 2 queries", + spec: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + map[string]interface{}{"refId": "B"}, + }, + }, + }, + }, + expected: 2, + }, + { + name: "multiple panels with queries", + spec: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + map[string]interface{}{ + "id": float64(2), + "type": "table", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + map[string]interface{}{"refId": "B"}, + map[string]interface{}{"refId": "C"}, + }, + }, + }, + }, + expected: 4, + }, + { + name: "queries in collapsed panels", + spec: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "row", + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(2), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + map[string]interface{}{"refId": "B"}, + }, + }, + }, + }, + }, + }, + expected: 2, + }, + { + name: "row panel with queries (should be ignored)", + spec: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + map[string]interface{}{ + "id": float64(2), + "type": "row", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, // Invalid - row panels shouldn't have queries + map[string]interface{}{"refId": "B"}, + }, + }, + }, + }, + expected: 1, // Only count the graph panel query, ignore row panel queries + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count := countQueriesV0V1(tt.spec) + assert.Equal(t, tt.expected, count) + }) + } +} + +func TestCountAnnotationsV0V1(t *testing.T) { + tests := []struct { + name string + spec map[string]interface{} + expected int + }{ + { + name: "nil spec", + spec: nil, + expected: 0, + }, + { + name: "dashboard with 2 annotations", + spec: map[string]interface{}{ + "annotations": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "Annotation 1"}, + map[string]interface{}{"name": "Annotation 2"}, + }, + }, + }, + expected: 2, + }, + { + name: "dashboard with no annotations", + spec: map[string]interface{}{ + "annotations": map[string]interface{}{ + "list": []interface{}{}, + }, + }, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count := countAnnotationsV0V1(tt.spec) + assert.Equal(t, tt.expected, count) + }) + } +} + +func TestCountLinksV0V1(t *testing.T) { + tests := []struct { + name string + spec map[string]interface{} + expected int + }{ + { + name: "nil spec", + spec: nil, + expected: 0, + }, + { + name: "dashboard with 3 links", + spec: map[string]interface{}{ + "links": []interface{}{ + map[string]interface{}{"title": "Link 1"}, + map[string]interface{}{"title": "Link 2"}, + map[string]interface{}{"title": "Link 3"}, + }, + }, + expected: 3, + }, + { + name: "dashboard with no links", + spec: map[string]interface{}{ + "links": []interface{}{}, + }, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count := countLinksV0V1(tt.spec) + assert.Equal(t, tt.expected, count) + }) + } +} + +func TestCountVariablesV0V1(t *testing.T) { + tests := []struct { + name string + spec map[string]interface{} + expected int + }{ + { + name: "nil spec", + spec: nil, + expected: 0, + }, + { + name: "empty spec", + spec: map[string]interface{}{}, + expected: 0, + }, + { + name: "dashboard with 3 variables", + spec: map[string]interface{}{ + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "var1", "type": "query"}, + map[string]interface{}{"name": "var2", "type": "custom"}, + map[string]interface{}{"name": "var3", "type": "datasource"}, + }, + }, + }, + expected: 3, + }, + { + name: "dashboard with no variables", + spec: map[string]interface{}{ + "templating": map[string]interface{}{ + "list": []interface{}{}, + }, + }, + expected: 0, + }, + { + name: "dashboard without templating field", + spec: map[string]interface{}{ + "title": "test", + }, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count := countVariablesV0V1(tt.spec) + assert.Equal(t, tt.expected, count) + }) + } +} + +func TestCountPanelsV2(t *testing.T) { + tests := []struct { + name string + elements map[string]dashv2alpha1.DashboardElement + expected int + }{ + { + name: "empty elements", + elements: map[string]dashv2alpha1.DashboardElement{}, + expected: 0, + }, + { + name: "2 regular panels", + elements: map[string]dashv2alpha1.DashboardElement{ + "panel1": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 1, + }, + }, + }, + "panel2": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 2, + }, + }, + }, + }, + expected: 2, + }, + { + name: "1 panel and 1 library panel", + elements: map[string]dashv2alpha1.DashboardElement{ + "panel1": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 1, + }, + }, + }, + "libpanel1": { + LibraryPanelKind: &dashv2alpha1.DashboardLibraryPanelKind{ + Kind: "LibraryPanel", + }, + }, + }, + expected: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count := countPanelsV2(tt.elements) + assert.Equal(t, tt.expected, count) + }) + } +} + +func TestDetectConversionDataLoss(t *testing.T) { + tests := []struct { + name string + sourceStats dashboardStats + targetStats dashboardStats + expectError bool + errorMsg string + }{ + { + name: "perfect match - no data loss", + sourceStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + variableCount: 2, + }, + targetStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + variableCount: 2, + }, + expectError: false, + }, + { + name: "panel count decreased (data loss)", + sourceStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + }, + targetStats: dashboardStats{ + panelCount: 2, // Lost a panel! + queryCount: 5, + annotationCount: 2, + linkCount: 1, + }, + expectError: true, + errorMsg: "panel count decreased", + }, + { + name: "panel count increased (allowed)", + sourceStats: dashboardStats{ + panelCount: 2, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + }, + targetStats: dashboardStats{ + panelCount: 3, // Added a panel (OK) + queryCount: 5, + annotationCount: 2, + linkCount: 1, + }, + expectError: false, + }, + { + name: "query count decreased (data loss)", + sourceStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + }, + targetStats: dashboardStats{ + panelCount: 3, + queryCount: 3, // Lost queries! + annotationCount: 2, + linkCount: 1, + }, + expectError: true, + errorMsg: "query count decreased", + }, + { + name: "annotation count decreased (data loss)", + sourceStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + }, + targetStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 1, // Lost annotation! + linkCount: 1, + }, + expectError: true, + errorMsg: "annotation count decreased", + }, + { + name: "annotation count increased (allowed - default annotations)", + sourceStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 0, + linkCount: 1, + }, + targetStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 1, // Added default annotation (OK) + linkCount: 1, + }, + expectError: false, + }, + { + name: "variable count decreased (data loss)", + sourceStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + variableCount: 3, + }, + targetStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + variableCount: 1, // Lost variables! + }, + expectError: true, + errorMsg: "variable count decreased", + }, + { + name: "multiple decreases (data loss)", + sourceStats: dashboardStats{ + panelCount: 3, + queryCount: 5, + annotationCount: 2, + linkCount: 1, + variableCount: 2, + }, + targetStats: dashboardStats{ + panelCount: 2, // Lost panel + queryCount: 3, // Lost queries + annotationCount: 2, + linkCount: 0, // Lost link + variableCount: 2, + }, + expectError: true, + errorMsg: "panel count decreased", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := detectConversionDataLoss(tt.sourceStats, tt.targetStats, "TestSource", "TestTarget") + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + + // Check if it's a ConversionDataLossError + var detectConversionDataLossErr *ConversionDataLossError + require.ErrorAs(t, err, &detectConversionDataLossErr) + assert.Equal(t, "TestSource_to_TestTarget", detectConversionDataLossErr.GetFunctionName()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestCollectStatsV0V1(t *testing.T) { + spec := map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + map[string]interface{}{"refId": "B"}, + }, + }, + map[string]interface{}{ + "id": float64(2), + "type": "table", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + }, + "annotations": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "Annotation 1"}, + }, + }, + "links": []interface{}{ + map[string]interface{}{"title": "Link 1"}, + map[string]interface{}{"title": "Link 2"}, + }, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "var1", "type": "query"}, + map[string]interface{}{"name": "var2", "type": "custom"}, + }, + }, + } + + stats := collectStatsV0V1(spec) + + assert.Equal(t, 2, stats.panelCount) + assert.Equal(t, 3, stats.queryCount) + assert.Equal(t, 1, stats.annotationCount) + assert.Equal(t, 2, stats.linkCount) + assert.Equal(t, 2, stats.variableCount) +} + +func TestCollectStatsV2alpha1(t *testing.T) { + spec := dashv2alpha1.DashboardSpec{ + Elements: map[string]dashv2alpha1.DashboardElement{ + "panel1": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 1, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{ + {}, + {}, + }, + }, + }, + }, + }, + }, + "panel2": { + PanelKind: &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: 2, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: []dashv2alpha1.DashboardPanelQueryKind{ + {}, + }, + }, + }, + }, + }, + }, + }, + Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{ + {}, + }, + Links: []dashv2alpha1.DashboardDashboardLink{ + {}, + {}, + }, + Variables: []dashv2alpha1.DashboardVariableKind{ + {}, + {}, + }, + } + + stats := collectStatsV2alpha1(spec) + + assert.Equal(t, 2, stats.panelCount) + assert.Equal(t, 3, stats.queryCount) + assert.Equal(t, 1, stats.annotationCount) + assert.Equal(t, 2, stats.linkCount) + assert.Equal(t, 2, stats.variableCount) +} + +func TestConversionDataLossError(t *testing.T) { + err := NewConversionDataLossError("TestFunc", "test error message", "v0alpha1", "v1beta1") + + assert.Equal(t, "TestFunc", err.GetFunctionName()) + assert.Equal(t, "v0alpha1", err.GetSourceAPIVersion()) + assert.Equal(t, "v1beta1", err.GetTargetAPIVersion()) + assert.Equal(t, "data loss detected in TestFunc (v0alpha1 → v1beta1): test error message", err.Error()) +} + +// Integration test showing how validation works with actual dashboard types +func TestWithConversionValidation_Integration(t *testing.T) { + // Create a source dashboard (v0) with specific counts + sourceV0 := &dashv0.Dashboard{ + Spec: dashv0.DashboardSpec{ + Object: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + map[string]interface{}{ + "id": float64(2), + "type": "table", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + map[string]interface{}{"refId": "B"}, + }, + }, + }, + "annotations": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "Annotation 1"}, + }, + }, + "links": []interface{}{ + map[string]interface{}{"title": "Link 1"}, + }, + }, + }, + } + + // Create a target dashboard (v1) with matching counts + targetV1 := &dashv1.Dashboard{ + Spec: dashv1.DashboardSpec{ + Object: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + }, + }, + map[string]interface{}{ + "id": float64(2), + "type": "table", + "targets": []interface{}{ + map[string]interface{}{"refId": "A"}, + map[string]interface{}{"refId": "B"}, + }, + }, + }, + "annotations": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"name": "Annotation 1"}, + }, + }, + "links": []interface{}{ + map[string]interface{}{"title": "Link 1"}, + }, + }, + }, + } + + // Mock conversion function that just copies the spec + mockConversion := func(a, b interface{}, scope conversion.Scope) error { + source := a.(*dashv0.Dashboard) + target := b.(*dashv1.Dashboard) + target.Spec = dashv1.DashboardSpec{Object: source.Spec.Object} + return nil + } + + // Wrap with validation + validatedFunc := withConversionDataLossDetection("V0", "V1beta1", mockConversion) + + // This should pass validation + err := validatedFunc(sourceV0, targetV1, nil) + assert.NoError(t, err) +} + +func TestWithConversionValidation_DataLoss(t *testing.T) { + // Create a source dashboard (v0) with 2 panels + sourceV0 := &dashv0.Dashboard{ + Spec: dashv0.DashboardSpec{ + Object: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + }, + map[string]interface{}{ + "id": float64(2), + "type": "table", + }, + }, + }, + }, + } + + targetV1 := &dashv1.Dashboard{} + + // Mock conversion function that loses a panel + mockBadConversion := func(a, b interface{}, scope conversion.Scope) error { + target := b.(*dashv1.Dashboard) + // Only copy 1 panel instead of 2 + target.Spec = dashv1.DashboardSpec{ + Object: map[string]interface{}{ + "panels": []interface{}{ + map[string]interface{}{ + "id": float64(1), + "type": "graph", + }, + // Missing panel 2! + }, + }, + } + return nil + } + + // Wrap with data loss detection + validatedFunc := withConversionDataLossDetection("V0", "V1beta1", mockBadConversion) + + // This should fail due to data loss + err := validatedFunc(sourceV0, targetV1, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "panel count decreased") + + var detectConversionDataLossErr *ConversionDataLossError + require.ErrorAs(t, err, &detectConversionDataLossErr) + assert.Equal(t, "V0_to_V1beta1", detectConversionDataLossErr.GetFunctionName()) +} + +// TestDataLossDetectionOnAllInputFiles tests all conversions from testdata/input +// and logs detailed information about missing panels when data loss is detected +func TestDataLossDetectionOnAllInputFiles(t *testing.T) { + // Initialize the migrator with a test data source provider + dsProvider := testutil.NewDataSourceProvider(testutil.StandardTestConfig) + migration.Initialize(dsProvider) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider) + require.NoError(t, err) + + // Read all files from input directory + files, err := os.ReadDir(filepath.Join("testdata", "input")) + require.NoError(t, err, "Failed to read input directory") + + for _, file := range files { + if file.IsDir() { + continue + } + + t.Run(file.Name(), func(t *testing.T) { + // Read input dashboard file + inputFile := filepath.Join("testdata", "input", file.Name()) + // ignore gosec G304 as this function is only used in the test process + //nolint:gosec + inputData, err := os.ReadFile(inputFile) + require.NoError(t, err, "Failed to read input file") + + // Parse the input dashboard to get its version + var rawDash map[string]interface{} + err = json.Unmarshal(inputData, &rawDash) + require.NoError(t, err, "Failed to unmarshal dashboard JSON") + + // Extract apiVersion + apiVersion, ok := rawDash["apiVersion"].(string) + require.True(t, ok, "apiVersion not found or not a string") + + // Parse group and version from apiVersion + gv, err := schema.ParseGroupVersion(apiVersion) + require.NoError(t, err) + + // Create source object based on version + var sourceDash runtime.Object + var sourceStats dashboardStats + + switch gv.Version { + case "v0alpha1": + var dash dashv0.Dashboard + err = json.Unmarshal(inputData, &dash) + require.NoError(t, err) + sourceDash = &dash + if dash.Spec.Object != nil { + sourceStats = collectStatsV0V1(dash.Spec.Object) + } + case "v1beta1": + var dash dashv1.Dashboard + err = json.Unmarshal(inputData, &dash) + require.NoError(t, err) + sourceDash = &dash + if dash.Spec.Object != nil { + sourceStats = collectStatsV0V1(dash.Spec.Object) + } + case "v2alpha1": + var dash dashv2alpha1.Dashboard + err = json.Unmarshal(inputData, &dash) + require.NoError(t, err) + sourceDash = &dash + sourceStats = collectStatsV2alpha1(dash.Spec) + case "v2beta1": + var dash dashv2beta1.Dashboard + err = json.Unmarshal(inputData, &dash) + require.NoError(t, err) + sourceDash = &dash + sourceStats = collectStatsV2beta1(dash.Spec) + default: + t.Fatalf("Unsupported source version: %s", gv.Version) + } + + // Get target versions from the dashboard manifest + manifest := apis.LocalManifest() + + // Get all Dashboard versions from the manifest + for _, kind := range manifest.ManifestData.Kinds() { + if kind.Kind == "Dashboard" { + for _, version := range kind.Versions { + // Skip converting to the same version + if version.VersionName == gv.Version { + continue + } + + targetVersion := version.VersionName + t.Run(fmt.Sprintf("to_%s", targetVersion), func(t *testing.T) { + // Create target object + var target runtime.Object + typeMeta := metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", dashv0.APIGroup, targetVersion), + Kind: "Dashboard", + } + + switch targetVersion { + case "v0alpha1": + target = &dashv0.Dashboard{TypeMeta: typeMeta} + case "v1beta1": + target = &dashv1.Dashboard{TypeMeta: typeMeta} + case "v2alpha1": + target = &dashv2alpha1.Dashboard{TypeMeta: typeMeta} + case "v2beta1": + target = &dashv2beta1.Dashboard{TypeMeta: typeMeta} + default: + t.Skipf("Unknown version %s", targetVersion) + return + } + + // Create a copy of the source for conversion + inputCopy := sourceDash.DeepCopyObject() + + // Convert to target version + err := scheme.Convert(inputCopy, target, nil) + + // Check if data loss was detected + var dataLossErr *ConversionDataLossError + if err != nil && errors.As(err, &dataLossErr) { + // Collect target stats + var targetStats dashboardStats + switch tgt := target.(type) { + case *dashv0.Dashboard: + if tgt.Spec.Object != nil { + targetStats = collectStatsV0V1(tgt.Spec.Object) + } + case *dashv1.Dashboard: + if tgt.Spec.Object != nil { + targetStats = collectStatsV0V1(tgt.Spec.Object) + } + case *dashv2alpha1.Dashboard: + targetStats = collectStatsV2alpha1(tgt.Spec) + case *dashv2beta1.Dashboard: + targetStats = collectStatsV2beta1(tgt.Spec) + } + + // Log detailed information about the data loss + t.Logf("🔍 DATA LOSS DETECTED: %s → %s", gv.Version, targetVersion) + t.Logf(" File: %s", file.Name()) + t.Logf(" Error: %s", err.Error()) + t.Logf("") + t.Logf(" 📊 Statistics:") + t.Logf(" Panels: %d → %d (Δ %d)", sourceStats.panelCount, targetStats.panelCount, targetStats.panelCount-sourceStats.panelCount) + t.Logf(" Queries: %d → %d (Δ %d)", sourceStats.queryCount, targetStats.queryCount, targetStats.queryCount-sourceStats.queryCount) + t.Logf(" Annotations: %d → %d (Δ %d)", sourceStats.annotationCount, targetStats.annotationCount, targetStats.annotationCount-sourceStats.annotationCount) + t.Logf(" Links: %d → %d (Δ %d)", sourceStats.linkCount, targetStats.linkCount, targetStats.linkCount-sourceStats.linkCount) + t.Logf("") + + // Log panel details if panels were lost + if targetStats.panelCount < sourceStats.panelCount { + t.Logf(" 📋 Panel Analysis:") + logMissingPanels(t, sourceDash, target, gv.Version, targetVersion) + } + + // Log query details if queries were lost + if targetStats.queryCount < sourceStats.queryCount { + t.Logf(" 🔍 Query Analysis:") + logMissingQueries(t, sourceDash, target, gv.Version, targetVersion) + } + + // Expected data loss for V2 → V0/V1 conversions (not yet implemented) + if strings.HasPrefix(gv.Version, "v2") && (targetVersion == "v0alpha1" || targetVersion == "v1beta1") { + t.Logf(" ℹ️ Note: V2 → %s conversions are not yet fully implemented - data loss is expected", targetVersion) + t.Skip("Skipping: V2 downgrade conversions not yet implemented") + } else { + // For other conversions, data loss is a real bug + t.Errorf("Unexpected data loss in %s → %s conversion", gv.Version, targetVersion) + } + } + }) + } + break + } + } + }) + } +} + +// logMissingPanels logs details about panels that were lost during conversion +func logMissingPanels(t *testing.T, source, target runtime.Object, sourceVersion, targetVersion string) { + t.Helper() + + // Extract source panels + var sourcePanelIDs []float64 + var sourcePanelTypes []string + + switch src := source.(type) { + case *dashv0.Dashboard: + if src.Spec.Object != nil { + sourcePanelIDs, sourcePanelTypes = extractPanelInfoV0V1(src.Spec.Object) + } + case *dashv1.Dashboard: + if src.Spec.Object != nil { + sourcePanelIDs, sourcePanelTypes = extractPanelInfoV0V1(src.Spec.Object) + } + case *dashv2alpha1.Dashboard: + sourcePanelIDs, sourcePanelTypes = extractPanelInfoV2alpha1(src.Spec) + case *dashv2beta1.Dashboard: + sourcePanelIDs, sourcePanelTypes = extractPanelInfoV2beta1(src.Spec) + } + + // Extract target panels + var targetPanelIDs []float64 + var targetPanelTypes []string + + switch tgt := target.(type) { + case *dashv0.Dashboard: + if tgt.Spec.Object != nil { + targetPanelIDs, targetPanelTypes = extractPanelInfoV0V1(tgt.Spec.Object) + } + case *dashv1.Dashboard: + if tgt.Spec.Object != nil { + targetPanelIDs, targetPanelTypes = extractPanelInfoV0V1(tgt.Spec.Object) + } + case *dashv2alpha1.Dashboard: + targetPanelIDs, targetPanelTypes = extractPanelInfoV2alpha1(tgt.Spec) + case *dashv2beta1.Dashboard: + targetPanelIDs, targetPanelTypes = extractPanelInfoV2beta1(tgt.Spec) + } + + // Create maps for quick lookup + targetIDMap := make(map[float64]bool) + for _, id := range targetPanelIDs { + targetIDMap[id] = true + } + + // Find missing panels + t.Logf(" Source panels (%s) - %d total:", sourceVersion, len(sourcePanelIDs)) + for i, id := range sourcePanelIDs { + panelType := "" + if i < len(sourcePanelTypes) { + panelType = sourcePanelTypes[i] + } + status := "✓" + if !targetIDMap[id] { + status = "✗ MISSING" + } + t.Logf(" %s Panel ID %.0f (type: %s)", status, id, panelType) + } + + t.Logf(" Target panels (%s) - %d total:", targetVersion, len(targetPanelIDs)) + for i, id := range targetPanelIDs { + panelType := "" + if i < len(targetPanelTypes) { + panelType = targetPanelTypes[i] + } + t.Logf(" Panel ID %.0f (type: %s)", id, panelType) + } +} + +// logMissingQueries logs details about queries that were lost during conversion +func logMissingQueries(t *testing.T, source, target runtime.Object, sourceVersion, targetVersion string) { + t.Helper() + + // Extract source query info + var sourceQueryInfo []queryInfo + + switch src := source.(type) { + case *dashv0.Dashboard: + if src.Spec.Object != nil { + sourceQueryInfo = extractQueryInfoV0V1(src.Spec.Object) + } + case *dashv1.Dashboard: + if src.Spec.Object != nil { + sourceQueryInfo = extractQueryInfoV0V1(src.Spec.Object) + } + case *dashv2alpha1.Dashboard: + sourceQueryInfo = extractQueryInfoV2alpha1(src.Spec) + case *dashv2beta1.Dashboard: + sourceQueryInfo = extractQueryInfoV2beta1(src.Spec) + } + + // Extract target query info + var targetQueryInfo []queryInfo + + switch tgt := target.(type) { + case *dashv0.Dashboard: + if tgt.Spec.Object != nil { + targetQueryInfo = extractQueryInfoV0V1(tgt.Spec.Object) + } + case *dashv1.Dashboard: + if tgt.Spec.Object != nil { + targetQueryInfo = extractQueryInfoV0V1(tgt.Spec.Object) + } + case *dashv2alpha1.Dashboard: + targetQueryInfo = extractQueryInfoV2alpha1(tgt.Spec) + case *dashv2beta1.Dashboard: + targetQueryInfo = extractQueryInfoV2beta1(tgt.Spec) + } + + t.Logf(" Source queries (%s) - %d total:", sourceVersion, len(sourceQueryInfo)) + for _, q := range sourceQueryInfo { + t.Logf(" Panel %.0f: %s (refId: %s)", q.panelID, q.datasourceType, q.refID) + } + + t.Logf(" Target queries (%s) - %d total:", targetVersion, len(targetQueryInfo)) + for _, q := range targetQueryInfo { + t.Logf(" Panel %.0f: %s (refId: %s)", q.panelID, q.datasourceType, q.refID) + } + + // Identify missing queries by comparing refIds per panel + missingCount := 0 + for _, srcQuery := range sourceQueryInfo { + found := false + for _, tgtQuery := range targetQueryInfo { + if srcQuery.panelID == tgtQuery.panelID && srcQuery.refID == tgtQuery.refID { + found = true + break + } + } + if !found { + if missingCount == 0 { + t.Logf("") + t.Logf(" ✗ Missing queries in %s → %s conversion:", sourceVersion, targetVersion) + } + t.Logf(" Panel %.0f: refId=%s, datasource=%s", srcQuery.panelID, srcQuery.refID, srcQuery.datasourceType) + missingCount++ + } + } +} + +// queryInfo contains information about a query for comparison +type queryInfo struct { + panelID float64 + refID string + datasourceType string +} + +// extractPanelInfoV0V1 extracts panel IDs and types from v0/v1 dashboards +func extractPanelInfoV0V1(spec map[string]interface{}) ([]float64, []string) { + if spec == nil { + return nil, nil + } + + panels, ok := spec["panels"].([]interface{}) + if !ok { + return nil, nil + } + + var ids []float64 + var types []string + + for _, p := range panels { + panelMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + + panelType, _ := panelMap["type"].(string) + + // Skip row panels, but process their collapsed panels + if panelType == "row" { + if collapsedPanels, ok := panelMap["panels"].([]interface{}); ok { + for _, cp := range collapsedPanels { + if cpMap, ok := cp.(map[string]interface{}); ok { + if id, ok := cpMap["id"].(float64); ok { + ids = append(ids, id) + cpType, _ := cpMap["type"].(string) + types = append(types, cpType) + } + } + } + } + } else { + // Regular panel + if id, ok := panelMap["id"].(float64); ok { + ids = append(ids, id) + types = append(types, panelType) + } + } + } + + return ids, types +} + +// extractPanelInfoV2alpha1 extracts panel IDs and types from v2alpha1 dashboards +func extractPanelInfoV2alpha1(spec dashv2alpha1.DashboardSpec) ([]float64, []string) { + var ids []float64 + var types []string + + for _, element := range spec.Elements { + if element.PanelKind != nil { + ids = append(ids, element.PanelKind.Spec.Id) + types = append(types, element.PanelKind.Spec.VizConfig.Kind) + } else if element.LibraryPanelKind != nil { + ids = append(ids, element.LibraryPanelKind.Spec.Id) + types = append(types, "LibraryPanel") + } + } + + return ids, types +} + +// extractPanelInfoV2beta1 extracts panel IDs and types from v2beta1 dashboards +func extractPanelInfoV2beta1(spec dashv2beta1.DashboardSpec) ([]float64, []string) { + var ids []float64 + var types []string + + for _, element := range spec.Elements { + if element.PanelKind != nil { + ids = append(ids, element.PanelKind.Spec.Id) + types = append(types, element.PanelKind.Spec.VizConfig.Kind) + } else if element.LibraryPanelKind != nil { + ids = append(ids, element.LibraryPanelKind.Spec.Id) + types = append(types, "LibraryPanel") + } + } + + return ids, types +} + +// extractQueryInfoV0V1 extracts query information from v0/v1 dashboards +// Note: Row panels should not have queries - we ignore queries on row panels themselves +func extractQueryInfoV0V1(spec map[string]interface{}) []queryInfo { + if spec == nil { + return nil + } + + panels, ok := spec["panels"].([]interface{}) + if !ok { + return nil + } + + var queries []queryInfo + + for _, p := range panels { + panelMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + + panelID, _ := panelMap["id"].(float64) + panelType, _ := panelMap["type"].(string) + + // Process regular panel queries (NOT row panels) + // Row panels are layout containers and should not have queries + if panelType != "row" { + if targets, ok := panelMap["targets"].([]interface{}); ok { + for _, target := range targets { + if targetMap, ok := target.(map[string]interface{}); ok { + refID, _ := targetMap["refId"].(string) + dsType := "" + if ds, ok := targetMap["datasource"].(map[string]interface{}); ok { + dsType, _ = ds["type"].(string) + } + queries = append(queries, queryInfo{ + panelID: panelID, + refID: refID, + datasourceType: dsType, + }) + } + } + } + } + + // Process queries in collapsed panels inside row panels + if panelType == "row" { + // Note: We ignore any queries on the row panel itself (panelMap["targets"]) + // and only process queries in the collapsed panels + if collapsedPanels, ok := panelMap["panels"].([]interface{}); ok { + for _, cp := range collapsedPanels { + if cpMap, ok := cp.(map[string]interface{}); ok { + cpID, _ := cpMap["id"].(float64) + if targets, ok := cpMap["targets"].([]interface{}); ok { + for _, target := range targets { + if targetMap, ok := target.(map[string]interface{}); ok { + refID, _ := targetMap["refId"].(string) + dsType := "" + if ds, ok := targetMap["datasource"].(map[string]interface{}); ok { + dsType, _ = ds["type"].(string) + } + queries = append(queries, queryInfo{ + panelID: cpID, + refID: refID, + datasourceType: dsType, + }) + } + } + } + } + } + } + } + } + + return queries +} + +// extractQueryInfoV2alpha1 extracts query information from v2alpha1 dashboards +func extractQueryInfoV2alpha1(spec dashv2alpha1.DashboardSpec) []queryInfo { + var queries []queryInfo + + for _, element := range spec.Elements { + if element.PanelKind != nil { + panelID := element.PanelKind.Spec.Id + for _, query := range element.PanelKind.Spec.Data.Spec.Queries { + refID := query.Spec.RefId + dsType := query.Kind + queries = append(queries, queryInfo{ + panelID: panelID, + refID: refID, + datasourceType: dsType, + }) + } + } + } + + return queries +} + +// extractQueryInfoV2beta1 extracts query information from v2beta1 dashboards +func extractQueryInfoV2beta1(spec dashv2beta1.DashboardSpec) []queryInfo { + var queries []queryInfo + + for _, element := range spec.Elements { + if element.PanelKind != nil { + panelID := element.PanelKind.Spec.Id + for _, query := range element.PanelKind.Spec.Data.Spec.Queries { + refID := query.Spec.RefId + dsType := query.Kind + queries = append(queries, queryInfo{ + panelID: panelID, + refID: refID, + datasourceType: dsType, + }) + } + } + } + + return queries +} diff --git a/apps/dashboard/pkg/migration/conversion/conversion_test.go b/apps/dashboard/pkg/migration/conversion/conversion_test.go index 2ea168e9522..9da0318dc81 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_test.go @@ -3,6 +3,7 @@ package conversion import ( "bytes" "encoding/json" + "errors" "fmt" "log/slog" "os" @@ -31,7 +32,8 @@ import ( func TestConversionMatrixExist(t *testing.T) { // Initialize the migrator with a test data source provider - migration.Initialize(migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)) + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) versions := []metav1.Object{ &dashv0.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}}, @@ -41,7 +43,7 @@ func TestConversionMatrixExist(t *testing.T) { } scheme := runtime.NewScheme() - err := RegisterConversions(scheme) + err := RegisterConversions(scheme, dsProvider) require.NoError(t, err) for idx, in := range versions { @@ -82,11 +84,12 @@ func TestDeepCopyValid(t *testing.T) { func TestDashboardConversionToAllVersions(t *testing.T) { // Initialize the migrator with a test data source provider - migration.Initialize(migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)) + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) // Set up conversion scheme scheme := runtime.NewScheme() - err := RegisterConversions(scheme) + err := RegisterConversions(scheme, dsProvider) require.NoError(t, err) // Read all files from input directory @@ -120,6 +123,17 @@ func TestDashboardConversionToAllVersions(t *testing.T) { require.NoError(t, err) require.Equal(t, dashv0.GROUP, gv.Group) + // Validate that the input file starts with the apiVersion declared in the object + expectedPrefix := fmt.Sprintf("%s.", gv.Version) + if !strings.HasPrefix(file.Name(), expectedPrefix) { + t.Fatalf( + "Input file %s does not match its declared apiVersion %s. "+ + "Expected filename to start with \"%s\". "+ + "Example: if apiVersion is \"dashboard.grafana.app/v1beta1\", "+ + "filename should start with \"v1beta1..json\"", + file.Name(), apiVersion, expectedPrefix) + } + // Create source object based on version var sourceDash metav1.Object switch gv.Version { @@ -199,6 +213,135 @@ func TestDashboardConversionToAllVersions(t *testing.T) { // Convert to target version err = scheme.Convert(inputCopy, target, nil) + + // Check if this is a V2→V0/V1 downgrade conversion (not yet implemented) + var dataLossErr *ConversionDataLossError + if err != nil && errors.As(err, &dataLossErr) { + // Check if this is a V2 downgrade + if strings.HasPrefix(gv.Version, "v2") && + (strings.Contains(filename, "v0alpha1") || strings.Contains(filename, "v1beta1")) { + // Write output file anyway for V2 downgrades (even with data loss) + // This helps with debugging and understanding what data is preserved + t.Logf("V2→V0/V1 conversion has expected data loss: %v", err) + testConversion(t, target.(metav1.Object), filename, outDir) + t.Skipf("V2→V0/V1 conversions not yet fully implemented - data loss expected") + return + } + // For non-V2-downgrade conversions, data loss is a real error + require.NoError(t, err, "Conversion failed for %s", filename) + } + + // Test the changes in the conversion result + testConversion(t, target.(metav1.Object), filename, outDir) + }) + } + }) + } +} + +// TestMigratedDashboardsConversion tests conversion of already-migrated dashboards +// from the migration package's latest_version output directory +func TestMigratedDashboardsConversion(t *testing.T) { + // Initialize the migrator with a test data source provider + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider) + require.NoError(t, err) + + // Read all files from migration package's latest_version directory + inputDir := filepath.Join("..", "testdata", "output", "latest_version") + files, err := os.ReadDir(inputDir) + require.NoError(t, err, "Failed to read latest_version directory") + + for _, file := range files { + if file.IsDir() { + continue + } + + t.Run(fmt.Sprintf("Convert_%s", file.Name()), func(t *testing.T) { + // Read input dashboard file + inputFile := filepath.Join(inputDir, file.Name()) + // ignore gosec G304 as this function is only used in the test process + //nolint:gosec + inputData, err := os.ReadFile(inputFile) + require.NoError(t, err, "Failed to read input file") + + // Parse the raw dashboard JSON + var rawDash map[string]interface{} + err = json.Unmarshal(inputData, &rawDash) + require.NoError(t, err, "Failed to unmarshal dashboard JSON") + + // These files are from the old migration system and are raw dashboard JSON + // We need to wrap them in the proper v1beta1 API structure + sourceDash := &dashv1.Dashboard{ + TypeMeta: metav1.TypeMeta{ + Kind: "Dashboard", + APIVersion: dashv1.APIVERSION, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: strings.TrimSuffix(file.Name(), ".json"), + }, + Spec: common.Unstructured{Object: rawDash}, + } + + // Ensure output directory exists + outDir := filepath.Join("testdata", "migrated_dashboards_output") + // ignore gosec G301 as this function is only used in the test process + //nolint:gosec + err = os.MkdirAll(outDir, 0755) + require.NoError(t, err, "Failed to create output directory") + + // Get target versions from the dashboard manifest + manifest := apis.LocalManifest() + targetVersions := make(map[string]runtime.Object) + + // Get original filename without extension + originalName := strings.TrimSuffix(file.Name(), ".json") + + // Get all Dashboard versions from the manifest + for _, kind := range manifest.ManifestData.Kinds() { + if kind.Kind == "Dashboard" { + for _, version := range kind.Versions { + // Skip v1beta1 since that's our source version + if version.VersionName == "v1beta1" { + continue + } + + // Prefix with v1beta1-mig- to indicate these came from v1beta1 dashboards + // that went through the migration pipeline + filename := fmt.Sprintf("v1beta1-mig-%s.%s.json", originalName, version.VersionName) + typeMeta := metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", dashv0.APIGroup, version.VersionName), + Kind: kind.Kind, // Dashboard + } + + // Create target object based on version + switch version.VersionName { + case "v0alpha1": + targetVersions[filename] = &dashv0.Dashboard{TypeMeta: typeMeta} + case "v2alpha1": + targetVersions[filename] = &dashv2alpha1.Dashboard{TypeMeta: typeMeta} + case "v2beta1": + targetVersions[filename] = &dashv2beta1.Dashboard{TypeMeta: typeMeta} + default: + t.Logf("Unknown version %s, skipping", version.VersionName) + } + } + break + } + } + + // Convert to each target version + for filename, target := range targetVersions { + t.Run(fmt.Sprintf("Convert_to_%s", filename), func(t *testing.T) { + // Create a copy of the input dashboard for conversion + inputCopy := sourceDash.DeepCopyObject() + + // Convert to target version + err := scheme.Convert(inputCopy, target, nil) require.NoError(t, err, "Conversion failed for %s", filename) // Test the changes in the conversion result @@ -234,7 +377,8 @@ func testConversion(t *testing.T, convertedDash metav1.Object, filename, outputD // TestConversionMetrics tests that conversion-level metrics are recorded correctly func TestConversionMetrics(t *testing.T) { // Initialize migration with test providers - migration.Initialize(migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)) + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -242,7 +386,7 @@ func TestConversionMetrics(t *testing.T) { // Set up conversion scheme scheme := runtime.NewScheme() - err := RegisterConversions(scheme) + err := RegisterConversions(scheme, dsProvider) require.NoError(t, err) tests := []struct { @@ -264,6 +408,7 @@ func TestConversionMetrics(t *testing.T) { Spec: common.Unstructured{Object: map[string]any{ "title": "test dashboard", "schemaVersion": 14, + "panels": []any{}, // Add empty panels array to avoid data loss detection issues }}, }, target: &dashv1.Dashboard{}, @@ -281,6 +426,7 @@ func TestConversionMetrics(t *testing.T) { Spec: common.Unstructured{Object: map[string]any{ "title": "test dashboard", "schemaVersion": 42, + "panels": []any{}, // Add empty panels array to avoid data loss detection issues }}, }, target: &dashv0.Dashboard{}, @@ -295,7 +441,12 @@ func TestConversionMetrics(t *testing.T) { name: "successful v2alpha1 to v2beta1 conversion", source: &dashv2alpha1.Dashboard{ ObjectMeta: metav1.ObjectMeta{UID: "test-uid-3"}, - Spec: dashv2alpha1.DashboardSpec{Title: "test dashboard"}, + Spec: dashv2alpha1.DashboardSpec{ + Title: "test dashboard", + Elements: map[string]dashv2alpha1.DashboardElement{}, // Add empty elements + Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{}, + Links: []dashv2alpha1.DashboardDashboardLink{}, + }, }, target: &dashv2beta1.Dashboard{}, expectAPISuccess: true, @@ -353,7 +504,8 @@ func TestConversionMetrics(t *testing.T) { // TestConversionMetricsWrapper tests the withConversionMetrics wrapper function func TestConversionMetricsWrapper(t *testing.T) { - migration.Initialize(migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)) + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -377,11 +529,18 @@ func TestConversionMetricsWrapper(t *testing.T) { Spec: common.Unstructured{Object: map[string]any{ "title": "test dashboard", "schemaVersion": 20, + "panels": []any{}, // Add empty panels array }}, }, target: &dashv1.Dashboard{}, conversionFunction: func(a, b interface{}, scope conversion.Scope) error { - // Simulate successful conversion + // Simulate successful conversion - need to set target panels too + tgt := b.(*dashv1.Dashboard) + tgt.Spec = common.Unstructured{Object: map[string]any{ + "title": "test dashboard", + "schemaVersion": 20, + "panels": []any{}, + }} return nil }, expectAPISuccess: true, @@ -397,6 +556,7 @@ func TestConversionMetricsWrapper(t *testing.T) { Spec: common.Unstructured{Object: map[string]any{ "title": "test dashboard", "schemaVersion": 30, + "panels": []any{}, // Add empty panels }}, }, target: &dashv0.Dashboard{}, @@ -404,8 +564,8 @@ func TestConversionMetricsWrapper(t *testing.T) { // Simulate conversion failure return fmt.Errorf("conversion failed") }, - expectAPISuccess: true, - expectMetricsSuccess: false, + expectAPISuccess: true, // wrapper returns nil to avoid 500 response + expectMetricsSuccess: false, // but still records failure metrics expectedSourceUID: "test-wrapper-2", expectedSourceAPI: dashv1.APIVERSION, expectedTargetAPI: dashv0.APIVERSION, @@ -449,11 +609,11 @@ func TestConversionMetricsWrapper(t *testing.T) { } if tt.expectAPISuccess && tt.expectMetricsSuccess { - require.Equal(t, float64(1), successTotal, "success metric should be incremented") + require.GreaterOrEqual(t, successTotal, float64(1), "success metric should be incremented") require.Equal(t, float64(0), failureTotal, "failure metric should not be incremented") } else { require.Equal(t, float64(0), successTotal, "success metric should not be incremented") - require.Equal(t, float64(1), failureTotal, "failure metric should be incremented") + require.GreaterOrEqual(t, failureTotal, float64(1), "failure metric should be incremented") } }) } @@ -512,7 +672,8 @@ func TestSchemaVersionExtraction(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Test the schema version extraction logic by creating a wrapper and checking the metrics labels - migration.Initialize(migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)) + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -555,7 +716,8 @@ func TestSchemaVersionExtraction(t *testing.T) { // TestConversionLogging tests that conversion-level logging works correctly func TestConversionLogging(t *testing.T) { - migration.Initialize(migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)) + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -563,7 +725,7 @@ func TestConversionLogging(t *testing.T) { // Set up conversion scheme scheme := runtime.NewScheme() - err := RegisterConversions(scheme) + err := RegisterConversions(scheme, dsProvider) require.NoError(t, err) tests := []struct { @@ -645,7 +807,8 @@ func TestConversionLogging(t *testing.T) { // TestConversionLogLevels tests that appropriate log levels are used func TestConversionLogLevels(t *testing.T) { - migration.Initialize(migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)) + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) t.Run("log levels and structured fields verification", func(t *testing.T) { // Create test wrapper to verify logging behavior @@ -695,7 +858,7 @@ func TestConversionLogLevels(t *testing.T) { target2 := &dashv0.Dashboard{} err = failureWrapper(source2, target2, nil) - require.NoError(t, err, "conversion wrapper should not error after recording logs") + require.NoError(t, err, "conversion wrapper returns nil to avoid 500 response, but logs error and records metrics") // The logging code paths are executed in both cases above // Success case logs at Debug level with fields: @@ -708,13 +871,15 @@ func TestConversionLogLevels(t *testing.T) { t.Log("✓ Failure logging uses Error level") t.Log("✓ All structured fields included in log messages") t.Log("✓ Dashboard UID extraction works for different dashboard types") + t.Log("✓ Wrapper returns nil to avoid 500 response even on errors") t.Log("✓ Schema version extraction handles various formats") }) } // TestConversionLoggingFields tests that all expected fields are included in log messages func TestConversionLoggingFields(t *testing.T) { - migration.Initialize(migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)) + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) t.Run("verify all log fields are present", func(t *testing.T) { // Test that the conversion wrapper includes all expected structured fields diff --git a/apps/dashboard/pkg/migration/conversion/metrics.go b/apps/dashboard/pkg/migration/conversion/metrics.go index 5bb7c895d81..5b2700bce60 100644 --- a/apps/dashboard/pkg/migration/conversion/metrics.go +++ b/apps/dashboard/pkg/migration/conversion/metrics.go @@ -3,6 +3,7 @@ package conversion import ( "errors" "fmt" + "math" "strings" "k8s.io/apimachinery/pkg/conversion" @@ -39,6 +40,11 @@ func getErroredConversionFunc(err error) string { return migrationErr.GetFunctionName() } + var dataLossErr *ConversionDataLossError + if errors.As(err, &dataLossErr) { + return dataLossErr.GetFunctionName() + } + return "" } @@ -65,6 +71,7 @@ func convertAPIVersionToFuncName(apiVersion string) string { } // withConversionMetrics wraps a conversion function with metrics and logging for the overall conversion process +// It optionally runs a data loss check function after successful conversion func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversionFunc func(a, b interface{}, scope conversion.Scope) error) func(a, b interface{}, scope conversion.Scope) error { return func(a, b interface{}, scope conversion.Scope) error { // Extract dashboard UID and schema version from source @@ -113,16 +120,24 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion // Execute the actual conversion err := conversionFunc(a, b, scope) + // If conversion succeeded, run data loss check + if err == nil { + err = checkConversionDataLoss(sourceVersionAPI, targetVersionAPI, a, b) + } + // Report conversion-level metrics and logs if err != nil { // Classify error type for metrics errorType := "conversion_error" var migrationErr *schemaversion.MigrationError var minVersionErr *schemaversion.MinimumVersionError + var dataLossErr *ConversionDataLossError if errors.As(err, &migrationErr) { errorType = "schema_version_migration_error" } else if errors.As(err, &minVersionErr) { errorType = "schema_minimum_version_error" + } else if errors.As(err, &dataLossErr) { + errorType = "conversion_data_loss_error" } // Record failure metrics @@ -161,6 +176,20 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion ) } + // Add data loss specific fields if this is a data loss error + if errorType == "conversion_data_loss_error" { + sourceStats := collectDashboardStats(a) + targetStats := collectDashboardStats(b) + // We consider losing data when the target is less than the source + logFields = append(logFields, + "panelsLost", math.Max(0, float64(sourceStats.panelCount-targetStats.panelCount)), + "queriesLost", math.Max(0, float64(sourceStats.queryCount-targetStats.queryCount)), + "annotationsLost", math.Max(0, float64(sourceStats.annotationCount-targetStats.annotationCount)), + "linksLost", math.Max(0, float64(sourceStats.linkCount-targetStats.linkCount)), + "variablesLost", math.Max(0, float64(sourceStats.variableCount-targetStats.variableCount)), + ) + } + // Add remaining fields logFields = append(logFields, "errorType", errorType, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.annotation-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.annotation-conversions.json new file mode 100644 index 00000000000..3b2b705cd5b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.annotation-conversions.json @@ -0,0 +1,217 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "annotation-conversions-test", + "labels": { + "test": "annotation-conversions" + } + }, + "spec": { + "title": "Annotation Conversions Test Dashboard", + "description": "Testing annotation transformations", + "tags": ["test", "annotations"], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "name": "Prometheus Annotations", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "target": { + "expr": "ALERTS{alertstate=\"firing\"}", + "refId": "Anno", + "interval": "1m", + "step": 60 + }, + "builtIn": 0, + "type": "prometheus" + }, + { + "name": "TestData Annotations", + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "target": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + }, + "builtIn": false, + "type": "testdata" + }, + { + "name": "Elasticsearch Annotations", + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "target": { + "query": "tags:deployment", + "timeField": "@timestamp", + "refId": "Anno" + }, + "textField": "message", + "tagsField": "tags", + "timeField": "@timestamp", + "timeEndField": "end_time", + "builtIn": 0, + "type": "elasticsearch" + }, + { + "name": "SQL Annotations", + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "enable": true, + "hide": false, + "iconColor": "green", + "target": { + "format": "table", + "rawSql": "SELECT time, title, text, tags FROM annotations WHERE $__timeFilter(time)", + "refId": "Anno" + }, + "textColumn": "text", + "titleColumn": "title", + "tagsColumn": "tags", + "timeColumn": "time", + "builtIn": 0, + "type": "sql" + }, + { + "name": "No Datasource Annotation", + "enable": true, + "hide": false, + "iconColor": "purple", + "target": { + "expr": "static_annotation_query", + "refId": "Anno" + }, + "builtIn": 0, + "type": "manual" + }, + { + "name": "Hidden Annotation", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "enable": false, + "hide": true, + "iconColor": "gray", + "target": { + "expr": "disabled_query", + "refId": "Anno" + }, + "builtIn": 0, + "type": "prometheus" + }, + { + "name": "Complex Filter Annotation", + "datasource": { + "type": "influxdb", + "uid": "influx-uid" + }, + "enable": true, + "hide": false, + "iconColor": "#FF5722", + "target": { + "query": "from(bucket: \"events\") |> range(start: $__timeFrom, stop: $__timeTo) |> filter(fn: (r) => r._measurement == \"deployments\")", + "refId": "Anno" + }, + "filter": { + "ids": [1, 2, 3], + "exclude": true + }, + "mappings": { + "title": "service", + "text": "description", + "time": "timestamp", + "tags": "labels" + }, + "builtIn": 0, + "type": "influxdb" + }, + { + "name": "CloudWatch Annotations", + "datasource": { + "type": "cloudwatch", + "uid": "cloudwatch-uid" + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "target": { + "namespace": "AWS/EC2", + "metricName": "CPUUtilization", + "dimensions": { + "InstanceId": "i-1234567890abcdef" + }, + "statistic": "Average", + "refId": "Anno" + }, + "showIn": 0, + "builtIn": 0, + "type": "cloudwatch" + }, + { + "name": "Disabled Built-in Annotation", + "builtIn": true, + "enable": false, + "hide": true, + "iconColor": "transparent", + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "target": { + "expr": "ALERTS{alertstate=\"firing\"}", + "refId": "Anno", + "interval": "1m", + "step": 60 + }, + "type": "prometheus" + } + ] + }, + "panels": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "links": [] + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.dashboard-properties.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.dashboard-properties.json new file mode 100644 index 00000000000..ccbb1ff372c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.dashboard-properties.json @@ -0,0 +1,171 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "dashboard-properties-test", + "labels": { + "test": "dashboard-properties" + } + }, + "spec": { + "title": "Dashboard Properties Test", + "description": "Testing dashboard-level property transformations including time settings, cursor sync, and links", + "tags": ["test", "properties", "metadata"], + "editable": true, + "preload": true, + "liveNow": true, + "graphTooltip": 2, + "fiscalYearStartMonth": 4, + "timezone": "America/New_York", + "weekStart": "sunday", + "refresh": "30s", + "time": { + "from": "now-12h", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "status": "Stable", + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], + "type": "timepicker", + "hidden": false, + "nowDelay": "1m" + }, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": ["monitoring", "alerts"], + "targetBlank": true, + "title": "External Monitoring System", + "tooltip": "View in external system", + "type": "link", + "url": "https://monitoring.example.com/dashboard?from=${__from}&to=${__to}" + }, + { + "asDropdown": true, + "icon": "dashboard", + "includeVars": false, + "keepTime": false, + "tags": ["grafana", "internal"], + "targetBlank": false, + "title": "Related Dashboards", + "tooltip": "Navigate to related dashboards", + "type": "dashboards", + "url": "" + }, + { + "asDropdown": false, + "icon": "info", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Documentation", + "tooltip": "View documentation", + "type": "link", + "url": "https://docs.example.com/dashboard-guide" + }, + { + "asDropdown": false, + "icon": "tag", + "includeVars": true, + "keepTime": true, + "tags": ["alerts"], + "targetBlank": false, + "title": "Tag-based Link", + "tooltip": "Filtered by tags", + "type": "tag", + "url": "/dashboards/tag/alerts" + } + ], + "panels": [ + { + "id": 1, + "title": "Simple Panel", + "type": "stat", + "gridPos": { + "x": 0, + "y": 0, + "w": 12, + "h": 8 + }, + "targets": [ + { + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "options": {} + } + ], + "templating": { + "list": [ + { + "name": "simple_var", + "type": "custom", + "query": "value1,value2,value3", + "current": { + "text": "value1", + "value": "value1" + }, + "hide": 0, + "skipUrlSync": false + } + ] + }, + "annotations": { + "list": [ + { + "name": "Simple Annotation", + "enable": true, + "hide": false, + "iconColor": "blue", + "target": { + "refId": "Anno" + }, + "builtIn": 0 + } + ] + }, + "style": "dark", + "version": 42, + "schemaVersion": 39, + "gnetId": 12345, + "iteration": 1640995200000, + "snapshot": { + "originalUrl": "http://localhost:3000/d/test", + "timestamp": "2023-01-01T00:00:00Z" + }, + "meta": { + "canEdit": true, + "canSave": true, + "canStar": true, + "canAdmin": false, + "slug": "dashboard-properties-test", + "url": "/d/test/dashboard-properties-test", + "expires": "2024-01-01T00:00:00Z", + "created": "2023-01-01T00:00:00Z", + "updated": "2023-06-01T00:00:00Z", + "updatedBy": "admin", + "createdBy": "admin", + "version": 42, + "hasAcl": false, + "isFolder": false, + "folderId": 0, + "folderTitle": "General", + "folderUrl": "", + "provisioned": false, + "provisionedExternalId": "" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-conversions.json new file mode 100644 index 00000000000..de3905eb4c9 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-conversions.json @@ -0,0 +1,340 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "panel-conversions-test", + "labels": { + "test": "panel-conversions" + } + }, + "spec": { + "title": "Panel Conversions Test Dashboard", + "description": "Testing panel to elements conversion", + "tags": ["test", "panels"], + "editable": true, + "preload": false, + "panels": [ + { + "cacheTimeout": "60s", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "interval": "1m", + "links": [ + { + "targetBlank": true, + "title": "External Link", + "url": "https://example.com" + } + ], + "maxDataPoints": 300, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "expr": "up", + "hide": false, + "interval": "30s", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "expr": "cpu_usage", + "hide": true, + "interval": "1m", + "refId": "B" + } + ], + "title": "Regular Timeseries Panel", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": ["mean"] + } + } + ], + "transparent": true, + "type": "timeseries" + }, + { + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 2, + "libraryPanel": { + "name": "Shared Stats Panel", + "uid": "library-panel-uid-123" + }, + "title": "Library Panel Reference" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "edprtf91hz01se" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 3, + "panels": [], + "repeat": "server", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "edprtf91hz01se" + }, + "refId": "A" + } + ], + "title": "Row Panel", + "type": "row" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 9 + }, + "id": 4, + "maxPerRow": 4, + "repeat": "region", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "Panel in Row", + "type": "timeseries" + }, + { + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "orientation": "horizontal" + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 16, + "x": 8, + "y": 9 + }, + "hideTimeOverride": true, + "id": 7, + "links": [ + { + "title": "Drill Down", + "url": "/d/detailed-view" + } + ], + "maxDataPoints": 1000, + "options": { + "barWidth": 0.97, + "groupWidth": 0.7, + "orientation": "horizontal" + }, + "pluginVersion": "9.5.0", + "queryCachingTTL": 300, + "targets": [ + { + "bucketAggs": [ + { + "field": "@timestamp", + "id": "2", + "type": "date_histogram" + } + ], + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "query": "*", + "refId": "A" + } + ], + "timeFrom": "1h", + "timeShift": "1h", + "title": "Complex Panel with All Features", + "transformations": [ + { + "id": "groupBy", + "options": { + "fields": { + "status": { + "aggregations": ["count"], + "operation": "groupby" + } + } + } + }, + { + "id": "sortBy", + "options": { + "sort": [ + { + "desc": true, + "field": "count" + } + ] + } + } + ], + "type": "barchart" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "edprtf91hz01se" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 5, + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 6, + "options": { + "showHeader": true + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "format": "table", + "rawSql": "SELECT * FROM metrics", + "refId": "A" + } + ], + "title": "Hidden Panel in Collapsed Row", + "type": "table" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "refId": "A" + } + ], + "title": "Collapsed Row", + "type": "row" + } + ], + "time": { + "from": "now-1h", + "to": "now" + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "links": [] + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json new file mode 100644 index 00000000000..a8d22b09eee --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json @@ -0,0 +1,331 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "variable-conversions-test", + "labels": { + "test": "variable-conversions" + } + }, + "spec": { + "title": "Variable Conversions Test Dashboard", + "description": "Testing all variable types and transformations", + "tags": ["test", "variables"], + "templating": { + "list": [ + { + "name": "datasource_var", + "type": "datasource", + "label": "Data Source", + "description": "Select a data source", + "query": "prometheus", + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus-uid" + }, + "options": [ + { + "selected": true, + "text": "Prometheus", + "value": "prometheus-uid" + }, + { + "selected": false, + "text": "InfluxDB", + "value": "influx-uid" + } + ], + "hide": 0, + "includeAll": false, + "multi": false, + "regex": "", + "skipUrlSync": false, + "refresh": 1 + }, + { + "name": "query_var", + "type": "query", + "label": "Instance", + "description": "Available instances", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "definition": "label_values(up, instance)", + "query": "label_values(up, instance)", + "current": { + "selected": true, + "text": ["All", "localhost:9090"], + "value": ["$__all", "localhost:9090"] + }, + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "localhost:9090", + "value": "localhost:9090" + }, + { + "selected": false, + "text": "localhost:9091", + "value": "localhost:9091" + } + ], + "hide": 1, + "includeAll": true, + "allValue": ".*", + "multi": true, + "regex": "/.*9090.*/", + "skipUrlSync": false, + "refresh": 2, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "useTags": false + }, + { + "name": "custom_var", + "type": "custom", + "label": "Environment", + "description": "Deployment environment", + "query": "prod,staging,dev", + "current": { + "selected": false, + "text": "prod", + "value": "prod" + }, + "options": [ + { + "selected": true, + "text": "prod", + "value": "prod" + }, + { + "selected": false, + "text": "staging", + "value": "staging" + }, + { + "selected": false, + "text": "dev", + "value": "dev" + } + ], + "hide": 0, + "includeAll": true, + "allValue": null, + "multi": false, + "skipUrlSync": false + }, + { + "name": "adhoc_var", + "type": "adhoc", + "label": "Filters", + "description": "Add filters", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "filters": [ + { + "key": "job", + "operator": "=", + "value": "prometheus" + } + ], + "baseFilters": [ + { + "key": "environment", + "operator": "=", + "value": "production" + } + ], + "hide": 0, + "skipUrlSync": false, + "defaultKeys": [ + { + "text": "job", + "value": "job" + }, + { + "text": "instance", + "value": "instance" + } + ], + "allowCustomValue": true + }, + { + "name": "constant_var", + "type": "constant", + "label": "App Name", + "description": "Application name constant", + "query": "my-application", + "current": { + "selected": false, + "text": "my-application", + "value": "my-application" + }, + "hide": 2, + "skipUrlSync": true + }, + { + "name": "interval_var", + "type": "interval", + "label": "Interval", + "description": "Time interval for queries", + "query": "1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "current": { + "selected": false, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": true, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + } + ], + "hide": 0, + "auto": true, + "auto_count": 30, + "auto_min": "10s", + "skipUrlSync": false, + "refresh": 2 + }, + { + "name": "textbox_var", + "type": "textbox", + "label": "Search Term", + "description": "Enter search term", + "query": "default_search", + "current": { + "selected": false, + "text": "error", + "value": "error" + }, + "hide": 0, + "skipUrlSync": false + }, + { + "name": "groupby_var", + "type": "groupby", + "label": "Group By", + "description": "Group results by field", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "query": "", + "current": { + "selected": true, + "text": ["job", "instance"], + "value": ["job", "instance"] + }, + "options": [ + { + "selected": true, + "text": "job", + "value": "job" + }, + { + "selected": true, + "text": "instance", + "value": "instance" + }, + { + "selected": false, + "text": "region", + "value": "region" + } + ], + "hide": 0, + "multi": true, + "skipUrlSync": false + }, + { + "name": "legacy_string_var", + "type": "query", + "label": "Legacy Variable", + "description": "Variable with legacy string value", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "query": "up", + "current": { + "selected": false, + "text": "legacy_value", + "value": "legacy_string_content" + }, + "hide": 0, + "refresh": 1, + "skipUrlSync": false + }, + { + "name": "complex_query_var", + "type": "query", + "label": "Complex Query Variable", + "description": "Variable with complex query and all features", + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "definition": "terms field:@host size:100", + "query": { + "query": "*", + "size": 100, + "bucketAggs": [ + { + "field": "@host", + "id": "2", + "type": "terms" + } + ] + }, + "current": { + "selected": true, + "text": ["All", "host1", "host2"], + "value": ["$__all", "host1", "host2"] + }, + "options": [], + "hide": 0, + "includeAll": true, + "allValue": "*", + "multi": true, + "regex": "/host[0-9]+/", + "skipUrlSync": false, + "refresh": 1, + "sort": 2, + "tagValuesQuery": "", + "tagsQuery": "", + "useTags": true + } + ] + }, + "panels": [], + "time": { + "from": "now-6h", + "to": "now" + }, + "annotations": { + "list": [] + }, + "links": [] + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v0alpha1.json new file mode 100644 index 00000000000..efc9da3ebbf --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v0alpha1.json @@ -0,0 +1,145 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v10.table_thresholds.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "styles": [ + { + "align": "auto", + "thresholds": [ + "20", + "30" + ] + }, + { + "align": "auto", + "thresholds": [ + "200", + "300" + ] + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "styles": [ + { + "align": "auto", + "thresholds": [ + "50", + "75" + ] + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "styles": [ + { + "thresholds": [ + "5", + "10", + "15" + ] + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V10 Table Thresholds Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2alpha1.json new file mode 100644 index 00000000000..2640aa14f7b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2alpha1.json @@ -0,0 +1,241 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v10.table_thresholds.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V10 Table Thresholds Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2beta1.json new file mode 100644 index 00000000000..69d75b09dcf --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2beta1.json @@ -0,0 +1,248 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v10.table_thresholds.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V10 Table Thresholds Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v0alpha1.json new file mode 100644 index 00000000000..f3f2829a10a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v0alpha1.json @@ -0,0 +1,127 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v11.no-op-migration.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries", + "yAxes": [ + { + "show": true + } + ] + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "server", + "options": [], + "query": "label_values(server)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V11 No-Op Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2alpha1.json new file mode 100644 index 00000000000..31afdc108a2 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2alpha1.json @@ -0,0 +1,210 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v11.no-op-migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V11 No-Op Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "server", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(server)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2beta1.json new file mode 100644 index 00000000000..053f7e18d9e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2beta1.json @@ -0,0 +1,217 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v11.no-op-migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V11 No-Op Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "server", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(server)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v0alpha1.json new file mode 100644 index 00000000000..26d7621741e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v0alpha1.json @@ -0,0 +1,94 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v12.template-variables.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "name": "refresh_true_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "name": "refresh_false_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "hide": 2, + "hideVariable": true, + "name": "hide_variable_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "hide": 1, + "hideLabel": true, + "name": "hide_label_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "hide": 2, + "hideLabel": true, + "hideVariable": true, + "name": "priority_var", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "name": "no_properties_var", + "options": [], + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V12 Template Variables Migration Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2alpha1.json new file mode 100644 index 00000000000..252a81e9796 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2alpha1.json @@ -0,0 +1,207 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v12.template-variables.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V12 Template Variables Migration Test", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "refresh_true_var", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "refresh_false_var", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "hide_variable_var", + "current": { + "text": "", + "value": "" + }, + "hide": "hideVariable", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "hide_label_var", + "current": { + "text": "", + "value": "" + }, + "hide": "hideLabel", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "priority_var", + "current": { + "text": "", + "value": "" + }, + "hide": "hideVariable", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "no_properties_var", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2beta1.json new file mode 100644 index 00000000000..82c17382c32 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2beta1.json @@ -0,0 +1,220 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v12.template-variables.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V12 Template Variables Migration Test", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "refresh_true_var", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "refresh_false_var", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "hide_variable_var", + "current": { + "text": "", + "value": "" + }, + "hide": "hideVariable", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "hide_label_var", + "current": { + "text": "", + "value": "" + }, + "hide": "hideLabel", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "priority_var", + "current": { + "text": "", + "value": "" + }, + "hide": "hideVariable", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "no_properties_var", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v0alpha1.json new file mode 100644 index 00000000000..4033b0f297f --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v0alpha1.json @@ -0,0 +1,187 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v13.graph_thresholds.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "threshold2": 400, + "threshold2Color": "red", + "thresholdLine": true + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph with Line Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "threshold1": 100, + "threshold1Color": "green", + "threshold2": 300, + "threshold2Color": "blue", + "thresholdLine": false + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph with Fill Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "threshold1": 150, + "threshold1Color": "orange", + "thresholdLine": true + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph with Single Threshold", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "threshold1": 200, + "threshold1Color": "yellow", + "thresholdLine": false + }, + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "thresholds": [ + { + "color": "purple", + "colorMode": "custom", + "value": 50 + } + ], + "title": "Graph with Existing Thresholds", + "type": "timeseries" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Non-Graph Panel", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V13 Graph Thresholds Migration Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2alpha1.json new file mode 100644 index 00000000000..49108c4b128 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2alpha1.json @@ -0,0 +1,355 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v13.graph_thresholds.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Graph with Line Thresholds", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Graph with Fill Thresholds", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Graph with Single Threshold", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Graph with Existing Thresholds", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Non-Graph Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V13 Graph Thresholds Migration Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2beta1.json new file mode 100644 index 00000000000..a41d04804b7 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2beta1.json @@ -0,0 +1,366 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v13.graph_thresholds.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Graph with Line Thresholds", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Graph with Fill Thresholds", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Graph with Single Threshold", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Graph with Existing Thresholds", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Non-Graph Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V13 Graph Thresholds Migration Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v0alpha1.json new file mode 100644 index 00000000000..691e08a7ef5 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v0alpha1.json @@ -0,0 +1,74 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v13.minimal_graph_config.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "id": 4, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "editorMode": "builder", + "expr": "{\"a.utf8.metric 🤘\", job=\"prometheus-utf8\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Dashboard with minimal graph panel settings", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2alpha1.json new file mode 100644 index 00000000000..c0ab55dcacf --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2alpha1.json @@ -0,0 +1,133 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v13.minimal_graph_config.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "editorMode": "builder", + "expr": "{\"a.utf8.metric 🤘\", job=\"prometheus-utf8\"}", + "instant": false, + "legendFormat": "__auto", + "range": true + } + }, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Dashboard with minimal graph panel settings", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2beta1.json new file mode 100644 index 00000000000..8e18ac60c4d --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2beta1.json @@ -0,0 +1,136 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v13.minimal_graph_config.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "editorMode": "builder", + "expr": "{\"a.utf8.metric 🤘\", job=\"prometheus-utf8\"}", + "instant": false, + "legendFormat": "__auto", + "range": true + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Dashboard with minimal graph panel settings", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v0alpha1.json new file mode 100644 index 00000000000..5dca5e74aaa --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v0alpha1.json @@ -0,0 +1,129 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v14.shared_crosshair_to_graph_tooltip.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "cpu_usage", + "refId": "A" + } + ], + "title": "CPU Usage Over Time", + "type": "timeseries", + "yAxes": [ + { + "show": true + } + ] + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "memory_usage", + "refId": "B" + } + ], + "title": "Memory Usage", + "type": "timeseries", + "yAxes": [ + { + "max": 100, + "min": 0, + "show": true + } + ] + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "server", + "options": [], + "query": "label_values(server)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V14 Shared Crosshair Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2alpha1.json new file mode 100644 index 00000000000..50d3e9aff2f --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2alpha1.json @@ -0,0 +1,214 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v14.shared_crosshair_to_graph_tooltip.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Crosshair", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage Over Time", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "cpu_usage" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "memory_usage" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-1h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V14 Shared Crosshair Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "server", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(server)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2beta1.json new file mode 100644 index 00000000000..8ee98acf062 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2beta1.json @@ -0,0 +1,221 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v14.shared_crosshair_to_graph_tooltip.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Crosshair", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage Over Time", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "cpu_usage" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "memory_usage" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-1h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V14 Shared Crosshair Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "server", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(server)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v0alpha1.json new file mode 100644 index 00000000000..6c259253e55 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v0alpha1.json @@ -0,0 +1,813 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v15.mimir_rollout_debugging.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "uid": "$datasource" + }, + "enable": true, + "expr": "up", + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "name": "rollouts", + "showIn": 0, + "tags": [], + "titleFormat": "Rollout was underway in {{cluster}}/{{namespace}}", + "type": "tags" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [ + { + "asDropdown": true, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": [ + "show-in-mimir-links-dropdown" + ], + "targetBlank": false, + "title": "Mimir dashboards", + "type": "dashboards" + } + ], + "panels": [ + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 19, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 7, + "panels": [], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Rollout progress", + "type": "row" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "### Versions running\nShows the versions reported by each running pod.\n\nThe rollout will fail if any pod is not running the expected version.\n\nPods in green are running the expected version, while pods running other versions are shown in orange.\n\n", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "orange", + "mode": "shades" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*(target)/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 19, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "percent", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xField": "job\\version", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum by (job, version) (up{job=~\".*\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Versions running", + "transformations": [ + { + "id": "groupingToMatrix", + "options": { + "columnField": "version", + "rowField": "job", + "valueField": "Value" + } + }, + { + "id": "sortBy", + "options": { + "sort": [ + { + "field": "job\\version" + } + ] + } + } + ], + "type": "barchart" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "### Deployment rollout progress\nShows the number of pods for each `Deployment` that match the desired configuration, as a proportion of the desired number of pods.\n\nThe rollout will fail if insufficient pods match the desired configuration for any `Deployment`.\n\nPods in green match the desired configuration, while pods that do not match the desired configuration are shown in orange.\n\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange" + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 19, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 2, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum by (deployment) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Deployment rollout progress", + "transformations": [ + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "deployment" + } + ] + } + } + ], + "type": "barchart" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "### StatefulSet rollout progress\nShows the number of pods for each `StatefulSet` that match the desired configuration, as a proportion of the desired number of pods.\n\nThe rollout will fail if insufficient pods match the desired configuration for any `StatefulSet`.\n\nPods in green match the desired configuration, while pods that do not match the desired configuration are shown in orange.\n\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange" + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 19, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 3, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum by (statefulset) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "StatefulSet rollout progress", + "transformations": [ + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "statefulset" + } + ] + } + } + ], + "type": "barchart" + }, + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 19, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 8, + "panels": [], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Rollout health", + "type": "row" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "### Aggregator lag\nShows the consumption lag of each aggregator pod.\n\nThis panel may show no data if aggregators are not deployed to this cell.\n\nThe rollout will fail if any pod's consumption lag is both:\n* greater than 30s (red area on graph), and\n* trending upwards compared to 1 minute earlier\n\n", + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "fillOpacity": 0, + "lineWidth": 1, + "pointSize": 5, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + }, + "min": 0, + "noValue": "No data (are aggregators deployed in this cell?)", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 30 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 19, + "w": 8, + "x": 0, + "y": 21 + }, + "id": 4, + "options": { + "legend": { + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "max by (pod) (up{job=\"mimir-aggregator\"})", + "format": "time_series", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Aggregator lag", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "### Unhealthy Deployment replicas\nShows the number of unavailable pods for each `Deployment`.\n\nThe rollout will fail if any `Deployment` has an unavailable pod.\n\nBoth this panel and the rollout check ignore any `Deployment`s that require spot nodes, as these are expected to be unavailable from time to time.\n\n`Deployment`s shown in green do not have any unavailable pods, while `Deployment`s shown in orange have one or more unavailable pods.\n\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 10, + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "orange", + "value": 1 + } + ] + }, + "unit": "" + }, + "overrides": [] + }, + "gridPos": { + "h": 19, + "w": 8, + "x": 8, + "y": 21 + }, + "id": 5, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum by (deployment) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Unhealthy Deployment replicas", + "transformations": [ + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "deployment" + } + ] + } + } + ], + "type": "barchart" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "### Unhealthy StatefulSet replicas\nShows the number of pods for each `StatefulSet` that are not ready.\n\nThe rollout will fail if any `StatefulSet` has fewer ready pods than requested.\n\nBoth this panel and the rollout check ignore any `StatefulSets`s that require spot nodes, as these are expected to be unavailable from time to time.\n\n`StatefulSets`s shown in green do not have any pods that are not ready, while `StatefulSet`s shown in orange have one or more pods that are not ready.\n\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 10, + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "orange", + "value": 1 + } + ] + }, + "unit": "" + }, + "overrides": [] + }, + "gridPos": { + "h": 19, + "w": 8, + "x": 16, + "y": 21 + }, + "id": 6, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum by (statefulset) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "Unhealthy StatefulSet replicas", + "transformations": [ + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "statefulset" + } + ] + } + } + ], + "type": "barchart" + } + ], + "refresh": "5m", + "schemaVersion": 42, + "tags": [ + "mimir", + "betterops-mimir", + "show-in-mimir-links-dropdown", + "as-code" + ], + "templating": { + "list": [ + { + "current": { + "value": "grafanacloud-prom" + }, + "hide": 0, + "label": "Data source", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "allValue": ".*", + "current": { + "text": "prod", + "value": "prod" + }, + "datasource": "$datasource", + "hide": 0, + "includeAll": true, + "label": "cluster", + "multi": false, + "name": "cluster", + "options": [], + "query": "label_values(up, job)", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "text": "prod", + "value": "prod" + }, + "datasource": "$datasource", + "hide": 0, + "includeAll": false, + "label": "namespace", + "multi": false, + "name": "namespace", + "options": [], + "query": "label_values(up{job=~\"$cluster\"}, instance)", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "utc", + "title": "Mimir / Rollout debugging", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2alpha1.json new file mode 100644 index 00000000000..5add17319b1 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2alpha1.json @@ -0,0 +1,1008 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v15.mimir_rollout_debugging.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "name": "rollouts", + "legacyOptions": { + "expr": "up", + "showIn": 0, + "tags": [], + "titleFormat": "Rollout was underway in {{cluster}}/{{namespace}}", + "type": "tags" + } + } + } + ], + "cursorSync": "Crosshair", + "editable": false, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Versions running", + "description": "### Versions running\nShows the versions reported by each running pod.\n\nThe rollout will fail if any pod is not running the expected version.\n\nPods in green are running the expected version, while pods running other versions are shown in orange.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sum by (job, version) (up{job=~\".*\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "datasource": { + "type": "", + "uid": "$datasource" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "groupingToMatrix", + "spec": { + "id": "groupingToMatrix", + "options": { + "columnField": "version", + "rowField": "job", + "valueField": "Value" + } + } + }, + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "sort": [ + { + "field": "job\\version" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "percent", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xField": "job\\version", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "min": 0, + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [] + }, + "color": { + "mode": "shades", + "fixedColor": "orange" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*(target)/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Deployment rollout progress", + "description": "### Deployment rollout progress\nShows the number of pods for each `Deployment` that match the desired configuration, as a proportion of the desired number of pods.\n\nThe rollout will fail if insufficient pods match the desired configuration for any `Deployment`.\n\nPods in green match the desired configuration, while pods that do not match the desired configuration are shown in orange.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sum by (deployment) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "datasource": { + "type": "", + "uid": "$datasource" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "deployment" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "min": 0, + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "orange" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "StatefulSet rollout progress", + "description": "### StatefulSet rollout progress\nShows the number of pods for each `StatefulSet` that match the desired configuration, as a proportion of the desired number of pods.\n\nThe rollout will fail if insufficient pods match the desired configuration for any `StatefulSet`.\n\nPods in green match the desired configuration, while pods that do not match the desired configuration are shown in orange.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sum by (statefulset) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "datasource": { + "type": "", + "uid": "$datasource" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "statefulset" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "min": 0, + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "orange" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Aggregator lag", + "description": "### Aggregator lag\nShows the consumption lag of each aggregator pod.\n\nThis panel may show no data if aggregators are not deployed to this cell.\n\nThe rollout will fail if any pod's consumption lag is both:\n* greater than 30s (red area on graph), and\n* trending upwards compared to 1 minute earlier\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "max by (pod) (up{job=\"mimir-aggregator\"})", + "format": "time_series", + "legendFormat": "__auto" + } + }, + "datasource": { + "type": "", + "uid": "$datasource" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 30, + "color": "red" + } + ] + }, + "noValue": "No data (are aggregators deployed in this cell?)", + "custom": { + "drawStyle": "line", + "fillOpacity": 0, + "lineWidth": 1, + "pointSize": 5, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Unhealthy Deployment replicas", + "description": "### Unhealthy Deployment replicas\nShows the number of unavailable pods for each `Deployment`.\n\nThe rollout will fail if any `Deployment` has an unavailable pod.\n\nBoth this panel and the rollout check ignore any `Deployment`s that require spot nodes, as these are expected to be unavailable from time to time.\n\n`Deployment`s shown in green do not have any unavailable pods, while `Deployment`s shown in orange have one or more unavailable pods.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sum by (deployment) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "datasource": { + "type": "", + "uid": "$datasource" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "deployment" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 1, + "color": "orange" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 10, + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Unhealthy StatefulSet replicas", + "description": "### Unhealthy StatefulSet replicas\nShows the number of pods for each `StatefulSet` that are not ready.\n\nThe rollout will fail if any `StatefulSet` has fewer ready pods than requested.\n\nBoth this panel and the rollout check ignore any `StatefulSets`s that require spot nodes, as these are expected to be unavailable from time to time.\n\n`StatefulSets`s shown in green do not have any pods that are not ready, while `StatefulSet`s shown in orange have one or more pods that are not ready.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sum by (statefulset) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "datasource": { + "type": "", + "uid": "$datasource" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "statefulset" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 1, + "color": "orange" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 10, + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Rollout progress", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Rollout health", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [ + { + "title": "Mimir dashboards", + "type": "dashboards", + "icon": "external link", + "tooltip": "", + "tags": [ + "show-in-mimir-links-dropdown" + ], + "asDropdown": true, + "targetBlank": false, + "includeVars": true, + "keepTime": true + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "mimir", + "betterops-mimir", + "show-in-mimir-links-dropdown", + "as-code" + ], + "timeSettings": { + "timezone": "utc", + "from": "now-1h", + "to": "now", + "autoRefresh": "5m", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Mimir / Rollout debugging", + "variables": [ + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource", + "pluginId": "prometheus", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "grafanacloud-prom" + }, + "options": [], + "multi": false, + "includeAll": false, + "label": "Data source", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "cluster", + "current": { + "text": "prod", + "value": "prod" + }, + "label": "cluster", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(up, job)" + } + }, + "regex": "", + "sort": "alphabeticalAsc", + "options": [], + "multi": false, + "includeAll": true, + "allValue": ".*", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "namespace", + "current": { + "text": "prod", + "value": "prod" + }, + "label": "namespace", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)" + } + }, + "regex": "", + "sort": "alphabeticalAsc", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2beta1.json new file mode 100644 index 00000000000..709c7f5f422 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2beta1.json @@ -0,0 +1,1027 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v15.mimir_rollout_debugging.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "name": "rollouts", + "legacyOptions": { + "expr": "up", + "showIn": 0, + "tags": [], + "titleFormat": "Rollout was underway in {{cluster}}/{{namespace}}", + "type": "tags" + } + } + } + ], + "cursorSync": "Crosshair", + "editable": false, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Versions running", + "description": "### Versions running\nShows the versions reported by each running pod.\n\nThe rollout will fail if any pod is not running the expected version.\n\nPods in green are running the expected version, while pods running other versions are shown in orange.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "$datasource" + }, + "spec": { + "expr": "sum by (job, version) (up{job=~\".*\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "groupingToMatrix", + "spec": { + "id": "groupingToMatrix", + "options": { + "columnField": "version", + "rowField": "job", + "valueField": "Value" + } + } + }, + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "sort": [ + { + "field": "job\\version" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "percent", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xField": "job\\version", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "min": 0, + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [] + }, + "color": { + "mode": "shades", + "fixedColor": "orange" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*(target)/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Deployment rollout progress", + "description": "### Deployment rollout progress\nShows the number of pods for each `Deployment` that match the desired configuration, as a proportion of the desired number of pods.\n\nThe rollout will fail if insufficient pods match the desired configuration for any `Deployment`.\n\nPods in green match the desired configuration, while pods that do not match the desired configuration are shown in orange.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "$datasource" + }, + "spec": { + "expr": "sum by (deployment) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "deployment" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "min": 0, + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "orange" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "StatefulSet rollout progress", + "description": "### StatefulSet rollout progress\nShows the number of pods for each `StatefulSet` that match the desired configuration, as a proportion of the desired number of pods.\n\nThe rollout will fail if insufficient pods match the desired configuration for any `StatefulSet`.\n\nPods in green match the desired configuration, while pods that do not match the desired configuration are shown in orange.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "$datasource" + }, + "spec": { + "expr": "sum by (statefulset) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "statefulset" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "min": 0, + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "orange" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Aggregator lag", + "description": "### Aggregator lag\nShows the consumption lag of each aggregator pod.\n\nThis panel may show no data if aggregators are not deployed to this cell.\n\nThe rollout will fail if any pod's consumption lag is both:\n* greater than 30s (red area on graph), and\n* trending upwards compared to 1 minute earlier\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "$datasource" + }, + "spec": { + "expr": "max by (pod) (up{job=\"mimir-aggregator\"})", + "format": "time_series", + "legendFormat": "__auto" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 30, + "color": "red" + } + ] + }, + "noValue": "No data (are aggregators deployed in this cell?)", + "custom": { + "drawStyle": "line", + "fillOpacity": 0, + "lineWidth": 1, + "pointSize": 5, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Unhealthy Deployment replicas", + "description": "### Unhealthy Deployment replicas\nShows the number of unavailable pods for each `Deployment`.\n\nThe rollout will fail if any `Deployment` has an unavailable pod.\n\nBoth this panel and the rollout check ignore any `Deployment`s that require spot nodes, as these are expected to be unavailable from time to time.\n\n`Deployment`s shown in green do not have any unavailable pods, while `Deployment`s shown in orange have one or more unavailable pods.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "$datasource" + }, + "spec": { + "expr": "sum by (deployment) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "deployment" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 1, + "color": "orange" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 10, + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Unhealthy StatefulSet replicas", + "description": "### Unhealthy StatefulSet replicas\nShows the number of pods for each `StatefulSet` that are not ready.\n\nThe rollout will fail if any `StatefulSet` has fewer ready pods than requested.\n\nBoth this panel and the rollout check ignore any `StatefulSets`s that require spot nodes, as these are expected to be unavailable from time to time.\n\n`StatefulSets`s shown in green do not have any pods that are not ready, while `StatefulSet`s shown in orange have one or more pods that are not ready.\n\n", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "$datasource" + }, + "spec": { + "expr": "sum by (statefulset) (up{job=\"kube-state-metrics\"})", + "format": "table", + "instant": true, + "legendFormat": "__auto" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "field": "statefulset" + } + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "displayMode": "list", + "showLegend": false + }, + "orientation": "horizontal", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "fieldConfig": { + "defaults": { + "unit": "", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 1, + "color": "orange" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 10, + "fillOpacity": 80, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Rollout progress", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Rollout health", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [ + { + "title": "Mimir dashboards", + "type": "dashboards", + "icon": "external link", + "tooltip": "", + "tags": [ + "show-in-mimir-links-dropdown" + ], + "asDropdown": true, + "targetBlank": false, + "includeVars": true, + "keepTime": true + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "mimir", + "betterops-mimir", + "show-in-mimir-links-dropdown", + "as-code" + ], + "timeSettings": { + "timezone": "utc", + "from": "now-1h", + "to": "now", + "autoRefresh": "5m", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Mimir / Rollout debugging", + "variables": [ + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource", + "pluginId": "prometheus", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "grafanacloud-prom" + }, + "options": [], + "multi": false, + "includeAll": false, + "label": "Data source", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "cluster", + "current": { + "text": "prod", + "value": "prod" + }, + "label": "cluster", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(up, job)" + } + }, + "regex": "", + "sort": "alphabeticalAsc", + "options": [], + "multi": false, + "includeAll": true, + "allValue": ".*", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "namespace", + "current": { + "text": "prod", + "value": "prod" + }, + "label": "namespace", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)" + } + }, + "regex": "", + "sort": "alphabeticalAsc", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v0alpha1.json new file mode 100644 index 00000000000..2b5625047da --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v0alpha1.json @@ -0,0 +1,135 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v15.no-op-migration.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "enable": true, + "name": "Annotations \u0026 Alerts" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries", + "yAxes": [ + { + "show": true + } + ] + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ] + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "server", + "options": [], + "query": "label_values(server)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V15 No-Op Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2alpha1.json new file mode 100644 index 00000000000..34d2ad77891 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2alpha1.json @@ -0,0 +1,227 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v15.no-op-migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "", + "name": "Annotations \u0026 Alerts" + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V15 No-Op Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "server", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(server)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2beta1.json new file mode 100644 index 00000000000..a2a031d5fa0 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2beta1.json @@ -0,0 +1,235 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v15.no-op-migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "grafana" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "", + "name": "Annotations \u0026 Alerts" + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V15 No-Op Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "server", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(server)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v0alpha1.json new file mode 100644 index 00000000000..649959487c2 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v0alpha1.json @@ -0,0 +1,1090 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v16.empty-rows-and-panels-array.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "description": "Sample monitoring dashboard for testing purposes.", + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 36098, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": [ + "sample-monitoring" + ], + "targetBlank": false, + "title": "Related dashboards", + "type": "dashboards", + "url": "" + } + ], + "panels": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample metric showing connection count.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 54, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "random_metric_alpha{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}", + "refId": "A" + } + ], + "title": "Alpha metric", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample counter metric.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "rate(sample_counter_beta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}", + "refId": "A" + } + ], + "title": "Beta counter", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample error metric.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "increase(test_errors_gamma{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}", + "refId": "A" + } + ], + "title": "Gamma errors", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample event rate metric.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "rate(demo_events_delta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}", + "refId": "A" + } + ], + "title": "Delta events", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample resource utilization metric.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 51, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "example_usage_epsilon{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{status}}", + "refId": "A" + } + ], + "title": "Epsilon usage", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample memory allocation metrics.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 51, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 16, + "x": 0, + "y": 24 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "random_total_zeta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - total", + "refId": "A" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "sample_target_eta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - target", + "refId": "B" + } + ], + "title": "Zeta and Eta", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample utilization percentage.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 24 + }, + "id": 8, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.1.7", + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "100 * random_total_zeta{env=~\"$env\", region=~\"$region\", node=~\"$node\"} / clamp_min(test_available_theta{env=~\"$env\", region=~\"$region\", node=~\"$node\"},1)", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}}", + "refId": "A" + } + ], + "title": "Theta utilization", + "type": "gauge" + }, + { + "collapsed": false, + "datasource": { + "uid": "${example_datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 9, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "refId": "A" + } + ], + "title": "Sample section", + "type": "row" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample latency metric for primary operations.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "increase(demo_latency_iota{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\", type=\"primary\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}", + "refId": "A" + } + ], + "title": "Iota primary latency", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample latency metric for secondary operations.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "increase(demo_latency_iota{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\", type=\"secondary\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}", + "refId": "A" + } + ], + "title": "Iota secondary latency", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${example_datasource}" + }, + "description": "Sample expansion events metric.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 49 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "uid": "${example_datasource}" + }, + "expr": "increase(example_events_kappa{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}", + "refId": "A" + } + ], + "title": "Kappa expansions", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 42, + "tags": [ + "sample-monitoring" + ], + "templating": { + "list": [ + { + "current": {}, + "hide": 0, + "label": "Data Source", + "name": "example_datasource", + "options": [], + "query": "sample_source", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "allValue": ".+", + "current": {}, + "datasource": { + "uid": "${example_datasource}" + }, + "hide": 0, + "includeAll": true, + "label": "Environment", + "multi": true, + "name": "env", + "options": [], + "query": "label_values(system_info_lambda{}, env)", + "refresh": 2, + "regex": "", + "sort": 2, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": {}, + "datasource": { + "uid": "${example_datasource}" + }, + "hide": 0, + "includeAll": true, + "label": "Region", + "multi": true, + "name": "region", + "options": [], + "query": "label_values(system_info_lambda{env=~\"$env\"}, region)", + "refresh": 2, + "regex": "", + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": {}, + "datasource": { + "uid": "${example_datasource}" + }, + "hide": 0, + "includeAll": true, + "label": "Node", + "multi": true, + "name": "node", + "options": [], + "query": "label_values(system_info_lambda{env=~\"$env\", region=~\"$region\"}, node)", + "refresh": 2, + "regex": "", + "sort": 2, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": {}, + "datasource": { + "uid": "${example_datasource}" + }, + "hide": 0, + "includeAll": true, + "label": "Service", + "multi": true, + "name": "service", + "options": [], + "query": "label_values(example_events_kappa{env=~\"$env\", region=~\"$region\", node=~\"$node\"}, service)", + "refresh": 2, + "regex": "", + "sort": 2, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "default", + "title": "Sample dashboard", + "uid": "sample-dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2alpha1.json new file mode 100644 index 00000000000..f94cd8566b2 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2alpha1.json @@ -0,0 +1,1459 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v16.empty-rows-and-panels-array.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "description": "Sample monitoring dashboard for testing purposes.", + "editable": false, + "elements": { + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Iota primary latency", + "description": "Sample latency metric for primary operations.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "increase(demo_latency_iota{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\", type=\"primary\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Iota secondary latency", + "description": "Sample latency metric for secondary operations.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "increase(demo_latency_iota{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\", type=\"secondary\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Kappa expansions", + "description": "Sample expansion events metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "increase(example_events_kappa{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Alpha metric", + "description": "Sample metric showing connection count.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "random_metric_alpha{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 54, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Beta counter", + "description": "Sample counter metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "rate(sample_counter_beta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "ops", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Gamma errors", + "description": "Sample error metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "increase(test_errors_gamma{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Delta events", + "description": "Sample event rate metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "rate(demo_events_delta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Epsilon usage", + "description": "Sample resource utilization metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "example_usage_epsilon{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{status}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "bytes", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 51, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Zeta and Eta", + "description": "Sample memory allocation metrics.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "random_total_zeta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - total" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sample_target_eta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - target" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "bytes", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 51, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Theta utilization", + "description": "Sample utilization percentage.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "100 * random_total_zeta{env=~\"$env\", region=~\"$region\", node=~\"$node\"} / clamp_min(test_available_theta{env=~\"$env\", region=~\"$region\", node=~\"$node\"},1)", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}}" + } + }, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "9.1.7", + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "min": 0, + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 16, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 24, + "width": 16, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 24, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Sample section", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [ + { + "title": "Related dashboards", + "type": "dashboards", + "icon": "external link", + "tooltip": "", + "url": "", + "tags": [ + "sample-monitoring" + ], + "asDropdown": false, + "targetBlank": false, + "includeVars": true, + "keepTime": true + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "sample-monitoring" + ], + "timeSettings": { + "timezone": "default", + "from": "now-30m", + "to": "now", + "autoRefresh": "30s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Sample dashboard", + "variables": [ + { + "kind": "DatasourceVariable", + "spec": { + "name": "example_datasource", + "pluginId": "sample_source", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "" + }, + "options": [], + "multi": false, + "includeAll": false, + "label": "Data Source", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "env", + "current": { + "text": "", + "value": "" + }, + "label": "Environment", + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(system_info_lambda{}, env)" + } + }, + "regex": "", + "sort": "alphabeticalDesc", + "options": [], + "multi": true, + "includeAll": true, + "allValue": ".+", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "region", + "current": { + "text": "", + "value": "" + }, + "label": "Region", + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(system_info_lambda{env=~\"$env\"}, region)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": true, + "allValue": ".*", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "node", + "current": { + "text": "", + "value": "" + }, + "label": "Node", + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(system_info_lambda{env=~\"$env\", region=~\"$region\"}, node)" + } + }, + "regex": "", + "sort": "alphabeticalDesc", + "options": [], + "multi": true, + "includeAll": true, + "allValue": ".+", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "service", + "current": { + "text": "", + "value": "" + }, + "label": "Service", + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "datasource": { + "type": "", + "uid": "${example_datasource}" + }, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(example_events_kappa{env=~\"$env\", region=~\"$region\", node=~\"$node\"}, service)" + } + }, + "regex": "", + "sort": "alphabeticalDesc", + "options": [], + "multi": true, + "includeAll": true, + "allValue": ".+", + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2beta1.json new file mode 100644 index 00000000000..d7f5fd82659 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2beta1.json @@ -0,0 +1,1485 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v16.empty-rows-and-panels-array.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "description": "Sample monitoring dashboard for testing purposes.", + "editable": false, + "elements": { + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Iota primary latency", + "description": "Sample latency metric for primary operations.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "increase(demo_latency_iota{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\", type=\"primary\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Iota secondary latency", + "description": "Sample latency metric for secondary operations.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "increase(demo_latency_iota{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\", type=\"secondary\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "s", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Kappa expansions", + "description": "Sample expansion events metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "increase(example_events_kappa{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Alpha metric", + "description": "Sample metric showing connection count.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "random_metric_alpha{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{service}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 54, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Beta counter", + "description": "Sample counter metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "rate(sample_counter_beta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "ops", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Gamma errors", + "description": "Sample error metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "increase(test_errors_gamma{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Delta events", + "description": "Sample event rate metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "rate(demo_events_delta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "1m", + "intervalFactor": 2, + "legendFormat": "{{node}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Epsilon usage", + "description": "Sample resource utilization metric.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "example_usage_epsilon{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - {{status}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "bytes", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 51, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Zeta and Eta", + "description": "Sample memory allocation metrics.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "random_total_zeta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - total" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "sample_target_eta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}} - target" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "unit": "bytes", + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 51, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Theta utilization", + "description": "Sample utilization percentage.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "expr": "100 * random_total_zeta{env=~\"$env\", region=~\"$region\", node=~\"$node\"} / clamp_min(test_available_theta{env=~\"$env\", region=~\"$region\", node=~\"$node\"},1)", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{node}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "9.1.7", + "spec": { + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "min": 0, + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 16, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 24, + "width": 16, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 24, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Sample section", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [ + { + "title": "Related dashboards", + "type": "dashboards", + "icon": "external link", + "tooltip": "", + "url": "", + "tags": [ + "sample-monitoring" + ], + "asDropdown": false, + "targetBlank": false, + "includeVars": true, + "keepTime": true + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "sample-monitoring" + ], + "timeSettings": { + "timezone": "default", + "from": "now-30m", + "to": "now", + "autoRefresh": "30s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Sample dashboard", + "variables": [ + { + "kind": "DatasourceVariable", + "spec": { + "name": "example_datasource", + "pluginId": "sample_source", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "" + }, + "options": [], + "multi": false, + "includeAll": false, + "label": "Data Source", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "env", + "current": { + "text": "", + "value": "" + }, + "label": "Environment", + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "__legacyStringValue": "label_values(system_info_lambda{}, env)" + } + }, + "regex": "", + "sort": "alphabeticalDesc", + "options": [], + "multi": true, + "includeAll": true, + "allValue": ".+", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "region", + "current": { + "text": "", + "value": "" + }, + "label": "Region", + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "__legacyStringValue": "label_values(system_info_lambda{env=~\"$env\"}, region)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": true, + "allValue": ".*", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "node", + "current": { + "text": "", + "value": "" + }, + "label": "Node", + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "__legacyStringValue": "label_values(system_info_lambda{env=~\"$env\", region=~\"$region\"}, node)" + } + }, + "regex": "", + "sort": "alphabeticalDesc", + "options": [], + "multi": true, + "includeAll": true, + "allValue": ".+", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "service", + "current": { + "text": "", + "value": "" + }, + "label": "Service", + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "datasource": { + "name": "${example_datasource}" + }, + "spec": { + "__legacyStringValue": "label_values(example_events_kappa{env=~\"$env\", region=~\"$region\", node=~\"$node\"}, service)" + } + }, + "regex": "", + "sort": "alphabeticalDesc", + "options": [], + "multi": true, + "includeAll": true, + "allValue": ".+", + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v0alpha1.json new file mode 100644 index 00000000000..b32592a5151 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v0alpha1.json @@ -0,0 +1,437 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v16.grid_layout_upgrade.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 13, + "panels": [], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "row" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "stat" + }, + { + "collapsed": true, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 14, + "panels": [ + { + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 3, + "title": "Process List", + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 17 + }, + "height": 200, + "id": 4, + "title": "Network I/O", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 17 + }, + "height": 200, + "id": 5, + "title": "Disk I/O", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Collapsed Row", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 15, + "panels": [], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Visible Row Title", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 18 + }, + "id": 6, + "maxPerRow": 6, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Temperature", + "type": "gauge" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 18 + }, + "id": 7, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Uptime", + "type": "stat" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 18 + }, + "id": 8, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Load Average", + "type": "bargauge" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 16, + "panels": [], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 4, + "w": 16, + "x": 0, + "y": 25 + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Description Panel", + "type": "text" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 16, + "y": 25 + }, + "height": 100, + "id": 10, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "System Logs", + "type": "logs" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 17, + "panels": [], + "repeat": "server", + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "row" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 11, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Server Metrics", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V16 Grid Layout Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2alpha1.json new file mode 100644 index 00000000000..c17e76fbc77 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2alpha1.json @@ -0,0 +1,723 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v16.grid_layout_upgrade.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "System Logs", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "logs", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Server Metrics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Process List", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Network I/O", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Disk I/O", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Temperature", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Uptime", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Load Average", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "bargauge", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Description Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 12, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 12, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Visible Row Title", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 16, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "repeat": { + "mode": "variable", + "value": "server" + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V16 Grid Layout Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2beta1.json new file mode 100644 index 00000000000..2a6622f27a4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2beta1.json @@ -0,0 +1,743 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v16.grid_layout_upgrade.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "System Logs", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "logs", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Server Metrics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Process List", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Network I/O", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Disk I/O", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Temperature", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Uptime", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Load Average", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "bargauge", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Description Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 12, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 12, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Visible Row Title", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 16, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "repeat": { + "mode": "variable", + "value": "server" + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V16 Grid Layout Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v0alpha1.json new file mode 100644 index 00000000000..047f8ec917b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v0alpha1.json @@ -0,0 +1,894 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v16.span_zero_demo.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [ + { + "icon": "external link", + "targetBlank": true, + "title": "External Documentation", + "type": "link", + "url": "https://example.com/docs" + } + ], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Application Monitoring", + "type": "text" + }, + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 23, + "panels": [], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Application Service", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 6, + "options": { + "content": "This service handles background processing tasks for the application system. It manages various types of operations including data synchronization, resource management, and batch processing.\n\nSupported operation types:\n1. Sync: Synchronizes data between different systems\n2. Process: Handles batch data processing tasks\n3. Cleanup: Removes outdated or temporary resources\n4. Update: Applies configuration changes across services\n\nService dependencies:\n- Data API: For reading and writing application data\n- Configuration Service: For managing system settings\n- Queue Service: For handling task scheduling\n- Storage Service: For persistent data management\n- Auth Service: For authentication and authorization\n- Metrics Service: For collecting operational statistics\n", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Service Overview", + "type": "text" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 7, + "options": { + "content": "Error monitoring helps identify issues in the system. This section displays error logs and success rates for operations.", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Error Monitoring", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "yellow", + "value": 0.95 + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 8, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum by (action) (app_jobs_processed_total{outcome=\"success\", cluster=\"$cluster\", namespace=\"default\"})\n/\nsum by (action) (app_jobs_processed_total{cluster=\"$cluster\", namespace=\"default\"})\n", + "legendFormat": "{{action}}", + "refId": "A" + } + ], + "title": "Job Success Rate", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 9, + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt | level=\"error\"", + "refId": "A" + } + ], + "title": "Errors", + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 10, + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt", + "refId": "A" + } + ], + "title": "All", + "type": "logs" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 11, + "options": { + "content": "Performance monitoring examines factors that affect system response times, including operation duration, queue lengths, and processing delays. This section provides metrics and traces for performance analysis.\n", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Performance Analysis", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Number of concurrent processing threads available for handling operations", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 12, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(app_worker_threads_active{cluster=\"$cluster\", namespace=\"default\"})", + "instant": true, + "refId": "A" + } + ], + "title": "Concurrent Job Drivers", + "type": "stat" + }, + { + "datasource": { + "type": "tempo", + "uid": "${tempo}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 15 + }, + "id": 13, + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "${tempo}" + }, + "filters": [ + { + "id": "span-name", + "operator": "=", + "scope": "span", + "tag": "name", + "value": [ + "provisioning.sync.process" + ] + }, + { + "id": "k8s-cluster-name", + "operator": "=", + "scope": "resource", + "tag": "k8s.cluster.name", + "value": [ + "$cluster" + ] + } + ], + "query": "{name=\"app.operation.process\"}", + "queryType": "traceqlSearch", + "refId": "A" + } + ], + "title": "Recent Operation Traces", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 2 + }, + { + "color": "red", + "value": 5 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 15 + }, + "id": 14, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.9, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}", + "refId": "E" + } + ], + "timeFrom": "7d", + "title": "7d avg of job durations", + "transformations": [ + { + "id": "reduce", + "options": { + "mode": "seriesToRows", + "reducers": [ + "mean" + ] + } + }, + { + "id": "seriesToRows" + }, + { + "id": "organize", + "options": { + "renameByName": { + "Field": "Type", + "Mean": "Avg Duration", + "Metric": "Legend", + "Value": "Duration" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 15, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.95, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}", + "refId": "E" + } + ], + "title": "Job Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "Total number of jobs waiting to be processed", + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 16, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "clamp_min(sum(app_operation_queue_size{cluster=\"$cluster\", namespace=\"default\"}), 0)", + "legendFormat": "Queue size", + "refId": "A" + } + ], + "title": "Queue Size", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "fieldConfig": { + "defaults": { + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 17, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "avg(histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le)))", + "legendFormat": "Queue size", + "refId": "A" + } + ], + "timeFrom": "7d", + "title": "7d avg Queue Wait Time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "description": "How long a job is in the queue before being picked up", + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 29 + }, + "id": 18, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.99, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.99", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.95, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.95", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.5", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "histogram_quantile(0.1, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.1", + "refId": "E" + } + ], + "title": "Queue Wait Time", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 29 + }, + "id": 19, + "options": { + "content": "Resource utilization monitoring for application containers", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Resource Monitoring", + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 29 + }, + "id": 20, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "count by (cluster, channel)(label_replace(label_replace(kube_pod_container_info{namespace=\"default\", container=\"app-worker\", pod=~\"app-worker.*\", cluster=~\"$cluster\"}, \"version\", \"$1\", \"image\", \".+:(.+)\"), \"channel\", \"$1\", \"container\", \".+-(.+)\"))", + "legendFormat": "{{cluster}}", + "refId": "A" + } + ], + "title": "Running Pod(s)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 21, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Request", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Limit", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(container_memory_usage_bytes{namespace=\"default\",cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"}) by (pod)", + "legendFormat": "Container usage {{pod}}", + "refId": "C" + } + ], + "title": "Memory Utilization", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 36 + }, + "id": 22, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum(irate(container_cpu_usage_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container, cpu)", + "legendFormat": "Usage {{pod}}", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "sum(irate(container_cpu_cfs_throttled_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container)", + "legendFormat": "Throttling {{pod}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU limit", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU request", + "refId": "D" + } + ], + "title": "CPU Utilization", + "type": "timeseries" + } + ], + "refresh": "10s", + "schemaVersion": 42, + "tags": [ + "as-code" + ], + "templating": { + "list": [ + { + "current": { + "value": "prometheus-datasource" + }, + "hide": 0, + "label": "Data source", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "value": "prometheus-datasource" + }, + "name": "prom", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "value": "loki-datasource" + }, + "name": "loki", + "options": [], + "query": "loki", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "current": { + "text": "tempo-datasource", + "value": "tempo-datasource" + }, + "name": "tempo", + "options": [], + "query": "tempo", + "refresh": 1, + "regex": ".*tempo.*", + "type": "datasource" + }, + { + "current": { + "text": "demo-cluster", + "value": "demo-cluster" + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "name": "cluster", + "options": [], + "query": "label_values(app_worker_threads_active,cluster)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "utc", + "title": "Span Zero Demo Dashboard", + "uid": "span-zero-demo-dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2alpha1.json new file mode 100644 index 00000000000..c8699b49e46 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2alpha1.json @@ -0,0 +1,1659 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v16.span_zero_demo.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": false, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Application Monitoring", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": { + "content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "All", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt" + } + }, + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "logs", + "spec": { + "pluginVersion": "", + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Performance Analysis", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": { + "content": "Performance monitoring examines factors that affect system response times, including operation duration, queue lengths, and processing delays. This section provides metrics and traces for performance analysis.\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Concurrent Job Drivers", + "description": "Number of concurrent processing threads available for handling operations", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "max(app_worker_threads_active{cluster=\"$cluster\", namespace=\"default\"})", + "instant": true + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "Recent Operation Traces", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "filters": [ + { + "id": "span-name", + "operator": "=", + "scope": "span", + "tag": "name", + "value": [ + "provisioning.sync.process" + ] + }, + { + "id": "k8s-cluster-name", + "operator": "=", + "scope": "resource", + "tag": "k8s.cluster.name", + "value": [ + "$cluster" + ] + } + ], + "query": "{name=\"app.operation.process\"}", + "queryType": "traceqlSearch" + } + }, + "datasource": { + "type": "tempo", + "uid": "${tempo}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-14": { + "kind": "Panel", + "spec": { + "id": 14, + "title": "7d avg of job durations", + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.9, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "D", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "E", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "mode": "seriesToRows", + "reducers": [ + "mean" + ] + } + } + }, + { + "kind": "seriesToRows", + "spec": { + "id": "seriesToRows", + "options": null + } + }, + { + "kind": "organize", + "spec": { + "id": "organize", + "options": { + "renameByName": { + "Field": "Type", + "Mean": "Avg Duration", + "Metric": "Legend", + "Value": "Duration" + } + } + } + } + ], + "queryOptions": { + "timeFrom": "7d" + } + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "s", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 2, + "color": "yellow" + }, + { + "value": 5, + "color": "red" + } + ] + } + }, + "overrides": [] + } + } + } + } + }, + "panel-15": { + "kind": "Panel", + "spec": { + "id": 15, + "title": "Job Duration", + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.95, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "D", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "E", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-16": { + "kind": "Panel", + "spec": { + "id": 16, + "title": "Queue Size", + "description": "Total number of jobs waiting to be processed", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "clamp_min(sum(app_operation_queue_size{cluster=\"$cluster\", namespace=\"default\"}), 0)", + "legendFormat": "Queue size" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-17": { + "kind": "Panel", + "spec": { + "id": 17, + "title": "7d avg Queue Wait Time", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "avg(histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le)))", + "legendFormat": "Queue size" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "timeFrom": "7d" + } + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "s" + }, + "overrides": [] + } + } + } + } + }, + "panel-18": { + "kind": "Panel", + "spec": { + "id": 18, + "title": "Queue Wait Time", + "description": "How long a job is in the queue before being picked up", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.99" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.95, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.95" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.5" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "D", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.1" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "E", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-19": { + "kind": "Panel", + "spec": { + "id": 19, + "title": "Resource Monitoring", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": { + "content": "Resource utilization monitoring for application containers", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-20": { + "kind": "Panel", + "spec": { + "id": 20, + "title": "Running Pod(s)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "count by (cluster, channel)(label_replace(label_replace(kube_pod_container_info{namespace=\"default\", container=\"app-worker\", pod=~\"app-worker.*\", cluster=~\"$cluster\"}, \"version\", \"$1\", \"image\", \".+:(.+)\"), \"channel\", \"$1\", \"container\", \".+-(.+)\"))", + "legendFormat": "{{cluster}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-21": { + "kind": "Panel", + "spec": { + "id": 21, + "title": "Memory Utilization", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Request" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Limit" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "max(container_memory_usage_bytes{namespace=\"default\",cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"}) by (pod)", + "legendFormat": "Container usage {{pod}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "C", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-22": { + "kind": "Panel", + "spec": { + "id": 22, + "title": "CPU Utilization", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sum(irate(container_cpu_usage_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container, cpu)", + "legendFormat": "Usage {{pod}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sum(irate(container_cpu_cfs_throttled_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container)", + "legendFormat": "Throttling {{pod}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU limit" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU request" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Service Overview", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": { + "content": "This service handles background processing tasks for the application system. It manages various types of operations including data synchronization, resource management, and batch processing.\n\nSupported operation types:\n1. Sync: Synchronizes data between different systems\n2. Process: Handles batch data processing tasks\n3. Cleanup: Removes outdated or temporary resources\n4. Update: Applies configuration changes across services\n\nService dependencies:\n- Data API: For reading and writing application data\n- Configuration Service: For managing system settings\n- Queue Service: For handling task scheduling\n- Storage Service: For persistent data management\n- Auth Service: For authentication and authorization\n- Metrics Service: For collecting operational statistics\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Error Monitoring", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": { + "content": "Error monitoring helps identify issues in the system. This section displays error logs and success rates for operations.", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Job Success Rate", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "sum by (action) (app_jobs_processed_total{outcome=\"success\", cluster=\"$cluster\", namespace=\"default\"})\n/\nsum by (action) (app_jobs_processed_total{cluster=\"$cluster\", namespace=\"default\"})\n", + "legendFormat": "{{action}}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 0.95, + "color": "yellow" + }, + { + "value": 1, + "color": "green" + } + ] + } + }, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Errors", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt | level=\"error\"" + } + }, + "datasource": { + "type": "loki", + "uid": "${loki}" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "logs", + "spec": { + "pluginVersion": "", + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Application Service", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 7, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 7, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 7, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 14, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 14, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 14, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-14" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 21, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-15" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 21, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-16" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 21, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-17" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 28, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-18" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 28, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-19" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 28, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-20" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 35, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-21" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 35, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-22" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [ + { + "title": "External Documentation", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "https://example.com/docs", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "as-code" + ], + "timeSettings": { + "timezone": "utc", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Span Zero Demo Dashboard", + "variables": [ + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource", + "pluginId": "prometheus", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "prometheus-datasource" + }, + "options": [], + "multi": false, + "includeAll": false, + "label": "Data source", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "prom", + "pluginId": "prometheus", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "prometheus-datasource" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "loki", + "pluginId": "loki", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "loki-datasource" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "tempo", + "pluginId": "tempo", + "refresh": "onDashboardLoad", + "regex": ".*tempo.*", + "current": { + "text": "tempo-datasource", + "value": "tempo-datasource" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "cluster", + "current": { + "text": "demo-cluster", + "value": "demo-cluster" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "datasource": { + "type": "prometheus", + "uid": "${prom}" + }, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(app_worker_threads_active,cluster)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2beta1.json new file mode 100644 index 00000000000..7c05e2ab74c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2beta1.json @@ -0,0 +1,1711 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v16.span_zero_demo.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": false, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Application Monitoring", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": { + "content": "This dashboard demonstrates various monitoring components for application observability and performance metrics.\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "All", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "${loki}" + }, + "spec": { + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "logs", + "version": "", + "spec": { + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Performance Analysis", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": { + "content": "Performance monitoring examines factors that affect system response times, including operation duration, queue lengths, and processing delays. This section provides metrics and traces for performance analysis.\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Concurrent Job Drivers", + "description": "Number of concurrent processing threads available for handling operations", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "max(app_worker_threads_active{cluster=\"$cluster\", namespace=\"default\"})", + "instant": true + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "Recent Operation Traces", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "tempo", + "version": "v0", + "datasource": { + "name": "${tempo}" + }, + "spec": { + "filters": [ + { + "id": "span-name", + "operator": "=", + "scope": "span", + "tag": "name", + "value": [ + "provisioning.sync.process" + ] + }, + { + "id": "k8s-cluster-name", + "operator": "=", + "scope": "resource", + "tag": "k8s.cluster.name", + "value": [ + "$cluster" + ] + } + ], + "query": "{name=\"app.operation.process\"}", + "queryType": "traceqlSearch" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-14": { + "kind": "Panel", + "spec": { + "id": 14, + "title": "7d avg of job durations", + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.9, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}" + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}" + } + }, + "refId": "D", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}" + } + }, + "refId": "E", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "mode": "seriesToRows", + "reducers": [ + "mean" + ] + } + } + }, + { + "kind": "seriesToRows", + "spec": { + "id": "seriesToRows", + "options": null + } + }, + { + "kind": "organize", + "spec": { + "id": "organize", + "options": { + "renameByName": { + "Field": "Type", + "Mean": "Avg Duration", + "Metric": "Legend", + "Value": "Duration" + } + } + } + } + ], + "queryOptions": { + "timeFrom": "7d" + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "s", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 2, + "color": "yellow" + }, + { + "value": 5, + "color": "red" + } + ] + } + }, + "overrides": [] + } + } + } + } + }, + "panel-15": { + "kind": "Panel", + "spec": { + "id": 15, + "title": "Job Duration", + "description": "Histogram showing p99, p95, p50, and p10 percentiles for job processing duration based on number of resources changed", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.95, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}" + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}" + } + }, + "refId": "D", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", + "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}" + } + }, + "refId": "E", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-16": { + "kind": "Panel", + "spec": { + "id": 16, + "title": "Queue Size", + "description": "Total number of jobs waiting to be processed", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "clamp_min(sum(app_operation_queue_size{cluster=\"$cluster\", namespace=\"default\"}), 0)", + "legendFormat": "Queue size" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-17": { + "kind": "Panel", + "spec": { + "id": 17, + "title": "7d avg Queue Wait Time", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "avg(histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le)))", + "legendFormat": "Queue size" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "timeFrom": "7d" + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "s" + }, + "overrides": [] + } + } + } + } + }, + "panel-18": { + "kind": "Panel", + "spec": { + "id": 18, + "title": "Queue Wait Time", + "description": "How long a job is in the queue before being picked up", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.99, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.99" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.95, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.95" + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.5" + } + }, + "refId": "D", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "histogram_quantile(0.1, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", + "legendFormat": "q0.1" + } + }, + "refId": "E", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-19": { + "kind": "Panel", + "spec": { + "id": 19, + "title": "Resource Monitoring", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": { + "content": "Resource utilization monitoring for application containers", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-20": { + "kind": "Panel", + "spec": { + "id": 20, + "title": "Running Pod(s)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "count by (cluster, channel)(label_replace(label_replace(kube_pod_container_info{namespace=\"default\", container=\"app-worker\", pod=~\"app-worker.*\", cluster=~\"$cluster\"}, \"version\", \"$1\", \"image\", \".+:(.+)\"), \"channel\", \"$1\", \"container\", \".+-(.+)\"))", + "legendFormat": "{{cluster}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-21": { + "kind": "Panel", + "spec": { + "id": 21, + "title": "Memory Utilization", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Request" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", + "legendFormat": "Memory Limit" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "max(container_memory_usage_bytes{namespace=\"default\",cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"}) by (pod)", + "legendFormat": "Container usage {{pod}}" + } + }, + "refId": "C", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-22": { + "kind": "Panel", + "spec": { + "id": 22, + "title": "CPU Utilization", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "sum(irate(container_cpu_usage_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container, cpu)", + "legendFormat": "Usage {{pod}}" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "sum(irate(container_cpu_cfs_throttled_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container)", + "legendFormat": "Throttling {{pod}}" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU limit" + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", + "legendFormat": "CPU request" + } + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Service Overview", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": { + "content": "This service handles background processing tasks for the application system. It manages various types of operations including data synchronization, resource management, and batch processing.\n\nSupported operation types:\n1. Sync: Synchronizes data between different systems\n2. Process: Handles batch data processing tasks\n3. Cleanup: Removes outdated or temporary resources\n4. Update: Applies configuration changes across services\n\nService dependencies:\n- Data API: For reading and writing application data\n- Configuration Service: For managing system settings\n- Queue Service: For handling task scheduling\n- Storage Service: For persistent data management\n- Auth Service: For authentication and authorization\n- Metrics Service: For collecting operational statistics\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Error Monitoring", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": { + "content": "Error monitoring helps identify issues in the system. This section displays error logs and success rates for operations.", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Job Success Rate", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "expr": "sum by (action) (app_jobs_processed_total{outcome=\"success\", cluster=\"$cluster\", namespace=\"default\"})\n/\nsum by (action) (app_jobs_processed_total{cluster=\"$cluster\", namespace=\"default\"})\n", + "legendFormat": "{{action}}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 0.95, + "color": "yellow" + }, + { + "value": 1, + "color": "green" + } + ] + } + }, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Errors", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "${loki}" + }, + "spec": { + "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt | level=\"error\"" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "logs", + "version": "", + "spec": { + "options": { + "enableLogDetails": true, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Application Service", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 0, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 7, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 7, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 7, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 14, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 14, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 14, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-14" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 21, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-15" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 21, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-16" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 21, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-17" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 28, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-18" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 28, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-19" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 28, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-20" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 35, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-21" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 35, + "width": 8, + "height": 7, + "element": { + "kind": "ElementReference", + "name": "panel-22" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [ + { + "title": "External Documentation", + "type": "link", + "icon": "external link", + "tooltip": "", + "url": "https://example.com/docs", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + } + ], + "liveNow": false, + "preload": false, + "tags": [ + "as-code" + ], + "timeSettings": { + "timezone": "utc", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Span Zero Demo Dashboard", + "variables": [ + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource", + "pluginId": "prometheus", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "prometheus-datasource" + }, + "options": [], + "multi": false, + "includeAll": false, + "label": "Data source", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "prom", + "pluginId": "prometheus", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "prometheus-datasource" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "loki", + "pluginId": "loki", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "", + "value": "loki-datasource" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "tempo", + "pluginId": "tempo", + "refresh": "onDashboardLoad", + "regex": ".*tempo.*", + "current": { + "text": "tempo-datasource", + "value": "tempo-datasource" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "cluster", + "current": { + "text": "demo-cluster", + "value": "demo-cluster" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "${prom}" + }, + "spec": { + "__legacyStringValue": "label_values(app_worker_threads_active,cluster)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v0alpha1.json new file mode 100644 index 00000000000..590ed8b7ed6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v0alpha1.json @@ -0,0 +1,322 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v17.minspan_to_maxperrow.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "maxPerRow": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with minSpan 8", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "maxPerRow": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with minSpan 12", + "type": "timeseries" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 2, + "maxPerRow": 6, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with minSpan 4", + "type": "stat" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 6, + "y": 8 + }, + "id": 6, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with minSpan 1", + "type": "stat" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 12, + "y": 8 + }, + "id": 7, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel without minSpan", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 8 + }, + "id": 8, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with invalid minSpan", + "type": "text" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 3, + "maxPerRow": 12, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with minSpan 2", + "type": "table" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 5, + "maxPerRow": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with minSpan 24", + "type": "gauge" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 22 + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with zero minSpan", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 22 + }, + "id": 10, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with negative minSpan", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V17 MinSpan to MaxPerRow Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2alpha1.json new file mode 100644 index 00000000000..9534b61db24 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2alpha1.json @@ -0,0 +1,640 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v17.minspan_to_maxperrow.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with minSpan 8", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Panel with negative minSpan", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with minSpan 4", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with minSpan 2", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with minSpan 12", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with minSpan 24", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with minSpan 1", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel without minSpan", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with invalid minSpan", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with zero minSpan", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 6, + "y": 8, + "width": 4, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 18, + "y": 8, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 12, + "width": 24, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 18, + "width": 24, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 22, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 6, + "y": 22, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V17 MinSpan to MaxPerRow Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2beta1.json new file mode 100644 index 00000000000..697c0dadaac --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2beta1.json @@ -0,0 +1,661 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v17.minspan_to_maxperrow.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with minSpan 8", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Panel with negative minSpan", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with minSpan 4", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with minSpan 2", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with minSpan 12", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with minSpan 24", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with minSpan 1", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel without minSpan", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with invalid minSpan", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with zero minSpan", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 6, + "y": 8, + "width": 4, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 8, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 18, + "y": 8, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 12, + "width": 24, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 18, + "width": 24, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 22, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 6, + "y": 22, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V17 MinSpan to MaxPerRow Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v0alpha1.json new file mode 100644 index 00000000000..fbcd020bf1c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v0alpha1.json @@ -0,0 +1,207 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v18.gauge_options.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "options": { + "thresholds": [ + { + "color": "red", + "value": 100 + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 0 + } + ], + "valueOptions": { + "decimals": 2, + "prefix": "Value: ", + "stat": "last", + "suffix": " ms", + "unit": "ms" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Complete Gauge Panel", + "type": "gauge" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "options": { + "valueOptions": { + "decimals": 1, + "unit": "percent" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Partial Gauge Panel", + "type": "gauge" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "options": { + "valueOptions": { + "decimals": 0, + "stat": "avg", + "unit": "bytes" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Buggy Gauge Panel", + "type": "gauge" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "options": { + "anotherProp": 42, + "customProperty": "customValue", + "thresholds": [ + { + "color": "blue", + "value": 10 + } + ], + "valueOptions": { + "unit": "short" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Custom Properties Gauge Panel", + "type": "gauge" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "options": { + "legend": { + "show": true, + "showLegend": true + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Non-Gauge Panel", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V18 Gauge Options Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2alpha1.json new file mode 100644 index 00000000000..7c22f7d6a45 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2alpha1.json @@ -0,0 +1,405 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v18.gauge_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Complete Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": { + "thresholds": [ + { + "color": "red", + "value": 100 + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 0 + } + ], + "valueOptions": { + "decimals": 2, + "prefix": "Value: ", + "stat": "last", + "suffix": " ms", + "unit": "ms" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Partial Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": { + "valueOptions": { + "decimals": 1, + "unit": "percent" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Buggy Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": { + "valueOptions": { + "decimals": 0, + "stat": "avg", + "unit": "bytes" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Custom Properties Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": { + "anotherProp": 42, + "customProperty": "customValue", + "thresholds": [ + { + "color": "blue", + "value": 10 + } + ], + "valueOptions": { + "unit": "short" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Non-Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "show": true, + "showLegend": true + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V18 Gauge Options Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2beta1.json new file mode 100644 index 00000000000..6d1528a868a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2beta1.json @@ -0,0 +1,416 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v18.gauge_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Complete Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": { + "thresholds": [ + { + "color": "red", + "value": 100 + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 0 + } + ], + "valueOptions": { + "decimals": 2, + "prefix": "Value: ", + "stat": "last", + "suffix": " ms", + "unit": "ms" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Partial Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": { + "valueOptions": { + "decimals": 1, + "unit": "percent" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Buggy Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": { + "valueOptions": { + "decimals": 0, + "stat": "avg", + "unit": "bytes" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Custom Properties Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": { + "anotherProp": 42, + "customProperty": "customValue", + "thresholds": [ + { + "color": "blue", + "value": 10 + } + ], + "valueOptions": { + "unit": "short" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Non-Gauge Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "show": true, + "showLegend": true + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V18 Gauge Options Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v0alpha1.json new file mode 100644 index 00000000000..77ad8639378 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v0alpha1.json @@ -0,0 +1,204 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v19.panel_links.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "links": [ + { + "title": "Dashboard Link", + "url": "dashboard/db/my-dashboard?$__url_time_range" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with legacy dashboard link", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "links": [ + { + "title": "DashUri Link", + "url": "dashboard/my-dashboard-uid?$__all_variables" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with dashUri link", + "type": "stat" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "links": [ + { + "title": "Custom Params Link", + "url": "http://example.com?customParam=value" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with custom params", + "type": "table" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "links": [ + { + "targetBlank": true, + "title": "Complex Link", + "url": "dashboard/db/complex-dashboard?$__url_time_range\u0026$__all_variables\u0026param1=value1\u0026param2=value2" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with complex link", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "links": [ + { + "title": "Existing URL Link", + "url": "http://existing-url.com" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with existing URL", + "type": "gauge" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 6, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with no links", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V19 Panel Links Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2alpha1.json new file mode 100644 index 00000000000..42f9b90a138 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2alpha1.json @@ -0,0 +1,438 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v19.panel_links.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with legacy dashboard link", + "description": "", + "links": [ + { + "title": "Dashboard Link", + "url": "dashboard/db/my-dashboard?$__url_time_range" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with dashUri link", + "description": "", + "links": [ + { + "title": "DashUri Link", + "url": "dashboard/my-dashboard-uid?$__all_variables" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with custom params", + "description": "", + "links": [ + { + "title": "Custom Params Link", + "url": "http://example.com?customParam=value" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with complex link", + "description": "", + "links": [ + { + "title": "Complex Link", + "url": "dashboard/db/complex-dashboard?$__url_time_range\u0026$__all_variables\u0026param1=value1\u0026param2=value2", + "targetBlank": true + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with existing URL", + "description": "", + "links": [ + { + "title": "Existing URL Link", + "url": "http://existing-url.com" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with no links", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V19 Panel Links Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2beta1.json new file mode 100644 index 00000000000..0dbf12ef898 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2beta1.json @@ -0,0 +1,451 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v19.panel_links.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with legacy dashboard link", + "description": "", + "links": [ + { + "title": "Dashboard Link", + "url": "dashboard/db/my-dashboard?$__url_time_range" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with dashUri link", + "description": "", + "links": [ + { + "title": "DashUri Link", + "url": "dashboard/my-dashboard-uid?$__all_variables" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with custom params", + "description": "", + "links": [ + { + "title": "Custom Params Link", + "url": "http://example.com?customParam=value" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with complex link", + "description": "", + "links": [ + { + "title": "Complex Link", + "url": "dashboard/db/complex-dashboard?$__url_time_range\u0026$__all_variables\u0026param1=value1\u0026param2=value2", + "targetBlank": true + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with existing URL", + "description": "", + "links": [ + { + "title": "Existing URL Link", + "url": "http://existing-url.com" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with no links", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V19 Panel Links Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v0alpha1.json new file mode 100644 index 00000000000..b799549c75d --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v0alpha1.json @@ -0,0 +1,181 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v2.panels-and-services.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graphite", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "max": 100, + "min": 0 + }, + "id": 1, + "legend": true, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "target": "cpu.usage" + } + ], + "title": "CPU Usage", + "type": "timeseries", + "y2_format": "short", + "y_format": "percent" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "min": 0 + }, + "id": 2, + "legend": false, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "target": "memory.usage" + } + ], + "title": "Memory Usage", + "type": "timeseries", + "y_format": "bytes" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "max": 100, + "min": 0 + }, + "id": 3, + "legend": true, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Server Stats", + "type": "table", + "y2_format": "bytes", + "y_format": "short" + }, + { + "autoMigrateFrom": "graphite", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "legend": true, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "target": "disk.io" + } + ], + "title": "Disk I/O", + "type": "timeseries", + "y2_format": "Bps" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "server", + "options": [], + "query": "label_values(server)", + "refresh": 1, + "type": "query" + }, + { + "name": "env", + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "stage" + } + ], + "type": "custom" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V2 Comprehensive Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2alpha1.json new file mode 100644 index 00000000000..2be443726d3 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2alpha1.json @@ -0,0 +1,358 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v2.panels-and-services.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "target": "cpu.usage" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "target": "memory.usage" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Server Stats", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Disk I/O", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "target": "disk.io" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V2 Comprehensive Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "server", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(server)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "env", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "Production", + "value": "prod" + }, + { + "selected": false, + "text": "Staging", + "value": "stage" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2beta1.json new file mode 100644 index 00000000000..6a82b1603f8 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2beta1.json @@ -0,0 +1,369 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v2.panels-and-services.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "target": "cpu.usage" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "target": "memory.usage" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Server Stats", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Disk I/O", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "target": "disk.io" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V2 Comprehensive Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "server", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(server)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "env", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "Production", + "value": "prod" + }, + { + "selected": false, + "text": "Staging", + "value": "stage" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v0alpha1.json new file mode 100644 index 00000000000..54910573b42 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v0alpha1.json @@ -0,0 +1,262 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v20.variable_syntax_links.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Link with series name", + "url": "http://example.com?series=${__series.name}\u0026timestamp=__value.time" + }, + { + "targetBlank": false, + "title": "Link with field name", + "url": "http://grafana.com/dashboard?field=__field.name\u0026series=${__series.name}" + } + ] + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with data links using legacy variable syntax", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": true, + "title": "Field link", + "url": "http://monitoring.com?field=${__field.name}\u0026series=__series.name" + }, + { + "targetBlank": false, + "title": "Time-based link", + "url": "http://logs.com?time=__value.time\u0026field=__field.name" + } + ], + "title": "Series: __series.name, Field: ${__field.name}, Time: __value.time" + } + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with field options using legacy variable syntax", + "type": "stat" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Combined link", + "url": "http://combined.com?series=${__series.name}\u0026field=${__field.name}\u0026time=__value.time" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Comprehensive link", + "url": "http://comprehensive.com?s=${__series.name}\u0026f=__field.name\u0026t=__value.time" + } + ], + "title": "Complete: __series.name / __field.name / __value.time" + } + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with both data links and field options", + "type": "gauge" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 4, + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Modern link", + "url": "http://modern.com?series=${__series.name}\u0026field=${__field.name}\u0026time=${__value.time}" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Modern field link", + "url": "http://modern-field.com?s=${__series.name}\u0026f=${__field.name}" + } + ], + "title": "Modern: ${__series.name} / ${__field.name} / ${__value.time}" + } + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with no legacy variables (should remain unchanged)", + "type": "table" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 5, + "options": { + "content": "This panel has no data links or field options to migrate." + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with no data links or field options", + "type": "text" + } + ], + "refresh": "5s", + "schemaVersion": 42, + "tags": [ + "migration-test" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V20 Variable Syntax Migration Test Dashboard", + "uid": "v20-migration-test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2alpha1.json new file mode 100644 index 00000000000..81ccea2ab37 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2alpha1.json @@ -0,0 +1,430 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v20.variable_syntax_links.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with data links using legacy variable syntax", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Link with series name", + "url": "http://example.com?series=${__series.name}\u0026timestamp=__value.time" + }, + { + "targetBlank": false, + "title": "Link with field name", + "url": "http://grafana.com/dashboard?field=__field.name\u0026series=${__series.name}" + } + ] + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with field options using legacy variable syntax", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": { + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": true, + "title": "Field link", + "url": "http://monitoring.com?field=${__field.name}\u0026series=__series.name" + }, + { + "targetBlank": false, + "title": "Time-based link", + "url": "http://logs.com?time=__value.time\u0026field=__field.name" + } + ], + "title": "Series: __series.name, Field: ${__field.name}, Time: __value.time" + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with both data links and field options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Combined link", + "url": "http://combined.com?series=${__series.name}\u0026field=${__field.name}\u0026time=__value.time" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Comprehensive link", + "url": "http://comprehensive.com?s=${__series.name}\u0026f=__field.name\u0026t=__value.time" + } + ], + "title": "Complete: __series.name / __field.name / __value.time" + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with no legacy variables (should remain unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Modern link", + "url": "http://modern.com?series=${__series.name}\u0026field=${__field.name}\u0026time=${__value.time}" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Modern field link", + "url": "http://modern-field.com?s=${__series.name}\u0026f=${__field.name}" + } + ], + "title": "Modern: ${__series.name} / ${__field.name} / ${__value.time}" + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with no data links or field options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": { + "content": "This panel has no data links or field options to migrate." + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 16, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 16, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "migration-test" + ], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "5s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V20 Variable Syntax Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2beta1.json new file mode 100644 index 00000000000..f5c9e157c75 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2beta1.json @@ -0,0 +1,441 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v20.variable_syntax_links.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with data links using legacy variable syntax", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Link with series name", + "url": "http://example.com?series=${__series.name}\u0026timestamp=__value.time" + }, + { + "targetBlank": false, + "title": "Link with field name", + "url": "http://grafana.com/dashboard?field=__field.name\u0026series=${__series.name}" + } + ] + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with field options using legacy variable syntax", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": { + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": true, + "title": "Field link", + "url": "http://monitoring.com?field=${__field.name}\u0026series=__series.name" + }, + { + "targetBlank": false, + "title": "Time-based link", + "url": "http://logs.com?time=__value.time\u0026field=__field.name" + } + ], + "title": "Series: __series.name, Field: ${__field.name}, Time: __value.time" + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with both data links and field options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Combined link", + "url": "http://combined.com?series=${__series.name}\u0026field=${__field.name}\u0026time=__value.time" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Comprehensive link", + "url": "http://comprehensive.com?s=${__series.name}\u0026f=__field.name\u0026t=__value.time" + } + ], + "title": "Complete: __series.name / __field.name / __value.time" + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with no legacy variables (should remain unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": { + "dataLinks": [ + { + "targetBlank": true, + "title": "Modern link", + "url": "http://modern.com?series=${__series.name}\u0026field=${__field.name}\u0026time=${__value.time}" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "targetBlank": false, + "title": "Modern field link", + "url": "http://modern-field.com?s=${__series.name}\u0026f=${__field.name}" + } + ], + "title": "Modern: ${__series.name} / ${__field.name} / ${__value.time}" + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with no data links or field options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": { + "content": "This panel has no data links or field options to migrate." + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 16, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 16, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "migration-test" + ], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "5s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V20 Variable Syntax Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v0alpha1.json new file mode 100644 index 00000000000..f0b61436447 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v0alpha1.json @@ -0,0 +1,206 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v21.data_links_series_to_field.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "options": { + "dataLinks": [ + { + "title": "Data Link 1", + "url": "http://mylink.com?series=${__field.labels}\u0026${__field.labels.a}" + }, + { + "title": "Data Link 2", + "url": "http://anotherlink.com?param=${__field.labels}" + } + ] + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with data links", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "options": { + "fieldOptions": { + "defaults": { + "links": [ + { + "title": "Field Link 1", + "url": "http://mylink.com?series=${__field.labels}\u0026${__field.labels.x}" + }, + { + "title": "Field Link 2", + "url": "http://fieldlink.com?field=${__field.labels}" + } + ] + } + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with field options links", + "type": "stat" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "options": { + "dataLinks": [ + { + "title": "Graph Data Link", + "url": "http://mylink.com?series=${__field.labels}" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "title": "Graph Field Link", + "url": "http://mylink.com?field=${__field.labels}" + } + ] + } + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with both link types", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "options": { + "dataLinks": [ + { + "title": "No Series Labels Link", + "url": "http://mylink.com?other=${__field.labels}" + } + ] + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel without series labels", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel without options", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V21 Data Links Series to Field Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2alpha1.json new file mode 100644 index 00000000000..32132d368c5 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2alpha1.json @@ -0,0 +1,405 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v21.data_links_series_to_field.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with data links", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "dataLinks": [ + { + "title": "Data Link 1", + "url": "http://mylink.com?series=${__field.labels}\u0026${__field.labels.a}" + }, + { + "title": "Data Link 2", + "url": "http://anotherlink.com?param=${__field.labels}" + } + ] + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with field options links", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": { + "fieldOptions": { + "defaults": { + "links": [ + { + "title": "Field Link 1", + "url": "http://mylink.com?series=${__field.labels}\u0026${__field.labels.x}" + }, + { + "title": "Field Link 2", + "url": "http://fieldlink.com?field=${__field.labels}" + } + ] + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with both link types", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "dataLinks": [ + { + "title": "Graph Data Link", + "url": "http://mylink.com?series=${__field.labels}" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "title": "Graph Field Link", + "url": "http://mylink.com?field=${__field.labels}" + } + ] + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel without series labels", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "dataLinks": [ + { + "title": "No Series Labels Link", + "url": "http://mylink.com?other=${__field.labels}" + } + ] + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel without options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V21 Data Links Series to Field Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2beta1.json new file mode 100644 index 00000000000..8c9808adf32 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2beta1.json @@ -0,0 +1,416 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v21.data_links_series_to_field.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with data links", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "dataLinks": [ + { + "title": "Data Link 1", + "url": "http://mylink.com?series=${__field.labels}\u0026${__field.labels.a}" + }, + { + "title": "Data Link 2", + "url": "http://anotherlink.com?param=${__field.labels}" + } + ] + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with field options links", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": { + "fieldOptions": { + "defaults": { + "links": [ + { + "title": "Field Link 1", + "url": "http://mylink.com?series=${__field.labels}\u0026${__field.labels.x}" + }, + { + "title": "Field Link 2", + "url": "http://fieldlink.com?field=${__field.labels}" + } + ] + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with both link types", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "dataLinks": [ + { + "title": "Graph Data Link", + "url": "http://mylink.com?series=${__field.labels}" + } + ], + "fieldOptions": { + "defaults": { + "links": [ + { + "title": "Graph Field Link", + "url": "http://mylink.com?field=${__field.labels}" + } + ] + } + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel without series labels", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "dataLinks": [ + { + "title": "No Series Labels Link", + "url": "http://mylink.com?other=${__field.labels}" + } + ] + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel without options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V21 Data Links Series to Field Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v0alpha1.json new file mode 100644 index 00000000000..a85ef5ebbda --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v0alpha1.json @@ -0,0 +1,83 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v22.table_panel_align.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "styles": [ + { + "align": "auto", + "pattern": "Time", + "type": "number" + }, + { + "align": "auto", + "pattern": "Value", + "type": "string" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V22 Table Panel Styles Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2alpha1.json new file mode 100644 index 00000000000..fbb8429153b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2alpha1.json @@ -0,0 +1,127 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v22.table_panel_align.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V22 Table Panel Styles Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2beta1.json new file mode 100644 index 00000000000..fd3dc0b587b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2beta1.json @@ -0,0 +1,130 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v22.table_panel_align.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V22 Table Panel Styles Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v0alpha1.json new file mode 100644 index 00000000000..07b650eb510 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v0alpha1.json @@ -0,0 +1,215 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v23.multi_variable_alignment.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "up", + "refId": "A" + } + ], + "title": "Test Panel", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "A" + ], + "value": [ + "A" + ] + }, + "datasource": "prometheus", + "multi": true, + "name": "multi_single_value", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": [ + "B", + "C" + ], + "value": [ + "B", + "C" + ] + }, + "datasource": "prometheus", + "multi": true, + "name": "multi_array_value", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "D", + "value": "D" + }, + "datasource": "prometheus", + "multi": false, + "name": "non_multi_array_value", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "E", + "value": "E" + }, + "datasource": "prometheus", + "multi": false, + "name": "non_multi_single_value", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "F", + "value": "F" + }, + "datasource": "prometheus", + "name": "no_multi_property", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "current": {}, + "datasource": "prometheus", + "multi": true, + "name": "empty_current", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": "prometheus", + "multi": true, + "name": "nil_current", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": [ + "G" + ], + "value": [ + "G" + ] + }, + "multi": true, + "name": "custom_variable", + "type": "custom" + }, + { + "current": { + "selected": true, + "text": "H", + "value": "H" + }, + "multi": false, + "name": "textbox_variable", + "type": "textbox" + }, + { + "current": { + "selected": true, + "text": [ + "Prometheus", + "InfluxDB" + ], + "value": [ + "prometheus", + "influxdb" + ] + }, + "multi": true, + "name": "datasource_variable", + "options": [], + "type": "datasource" + }, + { + "current": { + "selected": true, + "text": "1m", + "value": "1m" + }, + "multi": false, + "name": "interval_variable", + "type": "interval" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V23 Multi Variables Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2alpha1.json new file mode 100644 index 00000000000..7b4fe20e68e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2alpha1.json @@ -0,0 +1,378 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v23.multi_variable_alignment.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Test Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "up" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V23 Multi Variables Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "multi_single_value", + "current": { + "text": [ + "A" + ], + "value": [ + "A" + ] + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "multi_array_value", + "current": { + "text": [ + "B", + "C" + ], + "value": [ + "B", + "C" + ] + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "non_multi_array_value", + "current": { + "text": "D", + "value": "D" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "non_multi_single_value", + "current": { + "text": "E", + "value": "E" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "no_multi_property", + "current": { + "text": "F", + "value": "F" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "empty_current", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "nil_current", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_variable", + "query": "", + "current": { + "text": [ + "G" + ], + "value": [ + "G" + ] + }, + "options": [], + "multi": true, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "textbox_variable", + "current": { + "text": "H", + "value": "H" + }, + "query": "", + "hide": "dontHide", + "skipUrlSync": false + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource_variable", + "pluginId": "prometheus", + "refresh": "never", + "regex": "", + "current": { + "text": [ + "Prometheus", + "InfluxDB" + ], + "value": [ + "prometheus", + "influxdb" + ] + }, + "options": [], + "multi": true, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval_variable", + "query": "", + "current": { + "text": "1m", + "value": "1m" + }, + "options": [], + "auto": false, + "auto_min": "", + "auto_count": 0, + "refresh": "onTimeRangeChanged", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2beta1.json new file mode 100644 index 00000000000..8004109e441 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2beta1.json @@ -0,0 +1,395 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v23.multi_variable_alignment.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Test Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "up" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V23 Multi Variables Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "multi_single_value", + "current": { + "text": [ + "A" + ], + "value": [ + "A" + ] + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "multi_array_value", + "current": { + "text": [ + "B", + "C" + ], + "value": [ + "B", + "C" + ] + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "non_multi_array_value", + "current": { + "text": "D", + "value": "D" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "non_multi_single_value", + "current": { + "text": "E", + "value": "E" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "no_multi_property", + "current": { + "text": "F", + "value": "F" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "empty_current", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "nil_current", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": true, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_variable", + "query": "", + "current": { + "text": [ + "G" + ], + "value": [ + "G" + ] + }, + "options": [], + "multi": true, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "textbox_variable", + "current": { + "text": "H", + "value": "H" + }, + "query": "", + "hide": "dontHide", + "skipUrlSync": false + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource_variable", + "pluginId": "prometheus", + "refresh": "never", + "regex": "", + "current": { + "text": [ + "Prometheus", + "InfluxDB" + ], + "value": [ + "prometheus", + "influxdb" + ] + }, + "options": [], + "multi": true, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval_variable", + "query": "", + "current": { + "text": "1m", + "value": "1m" + }, + "options": [], + "auto": false, + "auto_min": "", + "auto_count": 0, + "refresh": "onTimeRangeChanged", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v0alpha1.json new file mode 100644 index 00000000000..fb3ee00d757 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v0alpha1.json @@ -0,0 +1,704 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v24.table-angular.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests basic migration with default style pattern (/.*/) containing thresholds and colors. Should convert styles to fieldConfig.defaults with threshold steps.", + "id": 1, + "legend": true, + "styles": [ + { + "colors": [ + "red", + "yellow", + "green" + ], + "pattern": "/.*/", + "thresholds": [ + "10", + "20", + "30" + ] + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B" + } + ], + "title": "Basic Angular Table with Defaults", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests comprehensive migration including: default style with thresholds/colors/unit/decimals/align/colorMode, column overrides with exact name and regex patterns, date formatting, hidden columns, and links with tooltips.", + "id": 2, + "styles": [ + { + "align": "center", + "colorMode": "cell", + "colors": [ + "green", + "yellow", + "red" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [ + "100", + "500" + ], + "unit": "bytes" + }, + { + "alias": "Current Status", + "align": "left", + "colorMode": "value", + "decimals": 0, + "pattern": "Status", + "unit": "short" + }, + { + "colorMode": "row", + "link": true, + "linkTargetBlank": true, + "linkTooltip": "View error details", + "linkUrl": "http://example.com/errors", + "pattern": "/Error.*/" + }, + { + "alias": "Timestamp", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "pattern": "Hidden", + "type": "hidden" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Complex Table with All Style Features", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "columns": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Maximum", + "value": "max" + }, + { + "text": "Minimum", + "value": "min" + }, + { + "text": "Total", + "value": "total" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Count", + "value": "count" + } + ], + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests migration of timeseries_aggregations transform to reduce transformation with column mappings (avg-\u003emean, max-\u003emax, min-\u003emin, total-\u003esum, current-\u003elastNotNull, count-\u003ecount).", + "id": 3, + "styles": [ + { + "decimals": 1, + "pattern": "/.*/", + "unit": "percent" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Table with Timeseries Aggregations Transform", + "transform": "timeseries_aggregations", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests migration of timeseries_to_rows transform to seriesToRows transformation.", + "id": 4, + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Table with Timeseries to Rows Transform", + "transform": "timeseries_to_rows", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests migration of timeseries_to_columns transform to seriesToColumns transformation.", + "id": 5, + "styles": [ + { + "pattern": "/.*/", + "unit": "bytes" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Table with Timeseries to Columns Transform", + "transform": "timeseries_to_columns", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests migration of table transform to merge transformation. Also tests auto alignment conversion to empty string.", + "id": 6, + "styles": [ + { + "align": "auto", + "pattern": "/.*/" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Table with Merge Transform", + "transform": "table", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests that existing transformations are preserved and new transformation from old format is appended to the list.", + "id": 7, + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Table with Existing Transformations", + "transform": "timeseries_to_rows", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "field1", + "field2" + ] + } + } + } + ], + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests handling of mixed numeric and string threshold values (int, string, float) with proper type conversion.", + "id": 8, + "styles": [ + { + "colors": [ + "green", + "yellow", + "orange", + "red" + ], + "pattern": "/.*/", + "thresholds": [ + 10, + "20", + 30.5 + ] + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Mixed Threshold Types", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests all color mode mappings: cell-\u003ecolor-background, row-\u003ecolor-background, value-\u003ecolor-text.", + "id": 9, + "styles": [ + { + "colorMode": "cell", + "pattern": "/.*/" + }, + { + "colorMode": "cell", + "pattern": "CellColumn" + }, + { + "colorMode": "row", + "pattern": "RowColumn" + }, + { + "colorMode": "value", + "pattern": "ValueColumn" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "All Color Modes Test", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests all alignment options: left, center, right, and auto (should convert to empty string).", + "id": 10, + "styles": [ + { + "align": "center", + "pattern": "/.*/" + }, + { + "align": "left", + "pattern": "LeftColumn" + }, + { + "align": "right", + "pattern": "RightColumn" + }, + { + "align": "auto", + "pattern": "AutoColumn" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "All Alignment Options", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests both field matcher types: byName for exact matches and byRegexp for regex patterns.", + "id": 11, + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + }, + { + "alias": "Exact Match", + "pattern": "ExactColumnName" + }, + { + "alias": "Regex Match", + "pattern": "/Regex.*Pattern/" + }, + { + "alias": "Start Pattern", + "pattern": "/^Start/" + }, + { + "alias": "End Pattern", + "pattern": "/End$/" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Field Matcher Types Test", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests various link configurations: with and without tooltip, with and without target blank.", + "id": 12, + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + }, + { + "link": true, + "linkTargetBlank": true, + "linkTooltip": "Click to view details", + "linkUrl": "http://example.com/with-tooltip", + "pattern": "LinkWithTooltip" + }, + { + "link": true, + "linkTargetBlank": false, + "linkUrl": "http://example.com/no-tooltip", + "pattern": "LinkWithoutTooltip" + }, + { + "link": true, + "linkUrl": "http://example.com/minimal", + "pattern": "LinkMinimal" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Link Configuration Test", + "type": "table" + }, + { + "autoMigrateFrom": "table-old", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests various date format patterns and aliases.", + "id": 13, + "styles": [ + { + "pattern": "/.*/", + "unit": "short" + }, + { + "alias": "ISO Date", + "dateFormat": "YYYY-MM-DD", + "pattern": "DateISO", + "type": "date" + }, + { + "alias": "Full DateTime", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "DateTime", + "type": "date" + }, + { + "alias": "Time Only", + "dateFormat": "HH:mm:ss", + "pattern": "TimeOnly", + "type": "date" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Date Format Variations", + "type": "table" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "React table (table2) should not be migrated. Properties should remain unchanged.", + "id": 14, + "styles": [ + { + "colors": [ + "red", + "yellow", + "green" + ], + "pattern": "/.*/", + "thresholds": [ + "10", + "20" + ], + "unit": "short" + } + ], + "table": "table2", + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "React Table - Should NOT Migrate", + "type": "table" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Angular table without styles property should not be migrated.", + "id": 15, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Angular Table without Styles - Should NOT Migrate", + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Non-table panels should remain completely unchanged.", + "id": 16, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Non-Table Panel - Should NOT Migrate", + "type": "timeseries" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Other panel types should not be affected by table migration.", + "id": 17, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Singlestat Panel - Should NOT Migrate", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "No Title", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2alpha1.json new file mode 100644 index 00000000000..6226b6c15e8 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2alpha1.json @@ -0,0 +1,1069 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v24.table-angular.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Basic Angular Table with Defaults", + "description": "Tests basic migration with default style pattern (/.*/) containing thresholds and colors. Should convert styles to fieldConfig.defaults with threshold steps.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "All Alignment Options", + "description": "Tests all alignment options: left, center, right, and auto (should convert to empty string).", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Field Matcher Types Test", + "description": "Tests both field matcher types: byName for exact matches and byRegexp for regex patterns.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Link Configuration Test", + "description": "Tests various link configurations: with and without tooltip, with and without target blank.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "Date Format Variations", + "description": "Tests various date format patterns and aliases.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-14": { + "kind": "Panel", + "spec": { + "id": 14, + "title": "React Table - Should NOT Migrate", + "description": "React table (table2) should not be migrated. Properties should remain unchanged.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-15": { + "kind": "Panel", + "spec": { + "id": 15, + "title": "Angular Table without Styles - Should NOT Migrate", + "description": "Angular table without styles property should not be migrated.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-16": { + "kind": "Panel", + "spec": { + "id": 16, + "title": "Non-Table Panel - Should NOT Migrate", + "description": "Non-table panels should remain completely unchanged.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-17": { + "kind": "Panel", + "spec": { + "id": 17, + "title": "Singlestat Panel - Should NOT Migrate", + "description": "Other panel types should not be affected by table migration.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Complex Table with All Style Features", + "description": "Tests comprehensive migration including: default style with thresholds/colors/unit/decimals/align/colorMode, column overrides with exact name and regex patterns, date formatting, hidden columns, and links with tooltips.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Table with Timeseries Aggregations Transform", + "description": "Tests migration of timeseries_aggregations transform to reduce transformation with column mappings (avg-\u003emean, max-\u003emax, min-\u003emin, total-\u003esum, current-\u003elastNotNull, count-\u003ecount).", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Table with Timeseries to Rows Transform", + "description": "Tests migration of timeseries_to_rows transform to seriesToRows transformation.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Table with Timeseries to Columns Transform", + "description": "Tests migration of timeseries_to_columns transform to seriesToColumns transformation.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Table with Merge Transform", + "description": "Tests migration of table transform to merge transformation. Also tests auto alignment conversion to empty string.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Table with Existing Transformations", + "description": "Tests that existing transformations are preserved and new transformation from old format is appended to the list.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "filterFieldsByName", + "spec": { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "field1", + "field2" + ] + } + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Mixed Threshold Types", + "description": "Tests handling of mixed numeric and string threshold values (int, string, float) with proper type conversion.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "All Color Modes Test", + "description": "Tests all color mode mappings: cell-\u003ecolor-background, row-\u003ecolor-background, value-\u003ecolor-text.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-14" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-15" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-16" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-17" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "No Title", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2beta1.json new file mode 100644 index 00000000000..3d74b493b8e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2beta1.json @@ -0,0 +1,1105 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v24.table-angular.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Basic Angular Table with Defaults", + "description": "Tests basic migration with default style pattern (/.*/) containing thresholds and colors. Should convert styles to fieldConfig.defaults with threshold steps.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "All Alignment Options", + "description": "Tests all alignment options: left, center, right, and auto (should convert to empty string).", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Field Matcher Types Test", + "description": "Tests both field matcher types: byName for exact matches and byRegexp for regex patterns.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Link Configuration Test", + "description": "Tests various link configurations: with and without tooltip, with and without target blank.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "Date Format Variations", + "description": "Tests various date format patterns and aliases.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-14": { + "kind": "Panel", + "spec": { + "id": 14, + "title": "React Table - Should NOT Migrate", + "description": "React table (table2) should not be migrated. Properties should remain unchanged.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-15": { + "kind": "Panel", + "spec": { + "id": 15, + "title": "Angular Table without Styles - Should NOT Migrate", + "description": "Angular table without styles property should not be migrated.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-16": { + "kind": "Panel", + "spec": { + "id": 16, + "title": "Non-Table Panel - Should NOT Migrate", + "description": "Non-table panels should remain completely unchanged.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-17": { + "kind": "Panel", + "spec": { + "id": 17, + "title": "Singlestat Panel - Should NOT Migrate", + "description": "Other panel types should not be affected by table migration.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Complex Table with All Style Features", + "description": "Tests comprehensive migration including: default style with thresholds/colors/unit/decimals/align/colorMode, column overrides with exact name and regex patterns, date formatting, hidden columns, and links with tooltips.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Table with Timeseries Aggregations Transform", + "description": "Tests migration of timeseries_aggregations transform to reduce transformation with column mappings (avg-\u003emean, max-\u003emax, min-\u003emin, total-\u003esum, current-\u003elastNotNull, count-\u003ecount).", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Table with Timeseries to Rows Transform", + "description": "Tests migration of timeseries_to_rows transform to seriesToRows transformation.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Table with Timeseries to Columns Transform", + "description": "Tests migration of timeseries_to_columns transform to seriesToColumns transformation.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Table with Merge Transform", + "description": "Tests migration of table transform to merge transformation. Also tests auto alignment conversion to empty string.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Table with Existing Transformations", + "description": "Tests that existing transformations are preserved and new transformation from old format is appended to the list.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "filterFieldsByName", + "spec": { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "field1", + "field2" + ] + } + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Mixed Threshold Types", + "description": "Tests handling of mixed numeric and string threshold values (int, string, float) with proper type conversion.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "All Color Modes Test", + "description": "Tests all color mode mappings: cell-\u003ecolor-background, row-\u003ecolor-background, value-\u003ecolor-text.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-14" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-15" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-16" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-17" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "No Title", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v0alpha1.json new file mode 100644 index 00000000000..463a09a3eb4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v0alpha1.json @@ -0,0 +1,125 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v25.no-op-migration.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "uid": "grafana" + }, + "enable": true, + "name": "Deployments" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with transformations remains unchanged", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph", + "type": "timeseries", + "yAxes": [ + { + "show": true + } + ] + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "tags should not be removed", + "options": [], + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V25 No-Op Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2alpha1.json new file mode 100644 index 00000000000..e83721eca1c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2alpha1.json @@ -0,0 +1,221 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v25.no-op-migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "", + "name": "Deployments" + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with transformations remains unchanged", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Graph", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V25 No-Op Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "tags should not be removed", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2beta1.json new file mode 100644 index 00000000000..7266cb6084c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2beta1.json @@ -0,0 +1,230 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v25.no-op-migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "", + "name": "Deployments" + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with transformations remains unchanged", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Graph", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V25 No-Op Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "tags should not be removed", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v0alpha1.json new file mode 100644 index 00000000000..b5af621776b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v0alpha1.json @@ -0,0 +1,117 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v26.text2_to_text.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Text2 Panel", + "type": "text" + }, + { + "content": "# Angular Text Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n\n", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "mode": "markdown", + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Angular Text Panel", + "type": "text" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "options": { + "content": "# React Text Panel from Angular Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n\n", + "mode": "markdown" + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "React Text Panel from Angular Panel", + "type": "text" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "No Title", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2alpha1.json new file mode 100644 index 00000000000..001fe0cf137 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2alpha1.json @@ -0,0 +1,244 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v26.text2_to_text.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Text2 Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Angular Text Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "React Text Panel from Angular Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": { + "content": "# React Text Panel from Angular Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "No Title", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2beta1.json new file mode 100644 index 00000000000..378ae69f16a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2beta1.json @@ -0,0 +1,251 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v26.text2_to_text.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Text2 Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Angular Text Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "React Text Panel from Angular Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": { + "content": "# React Text Panel from Angular Panel\n# $constant\n\nFor markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n\n## $text\n\n", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "No Title", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v0alpha1.json new file mode 100644 index 00000000000..8c13baae82a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v0alpha1.json @@ -0,0 +1,119 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v27.repeated_panels_and_constant_variable.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Normal Panel", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "panels": [ + { + "autoMigrateFrom": "graph", + "id": 6, + "title": "Normal nested panel", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Row with repeated panels", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "default_value", + "value": "default_value" + }, + "hide": 0, + "name": "constant_var", + "options": [ + { + "selected": true, + "text": "default_value", + "value": "default_value" + } + ], + "query": "default_value", + "type": "textbox" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V27 Repeated Panels and Constant Variable Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2alpha1.json new file mode 100644 index 00000000000..c722d627187 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2alpha1.json @@ -0,0 +1,211 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v27.repeated_panels_and_constant_variable.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Normal Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Normal nested panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with repeated panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V27 Repeated Panels and Constant Variable Migration Test Dashboard", + "variables": [ + { + "kind": "TextVariable", + "spec": { + "name": "constant_var", + "current": { + "text": "default_value", + "value": "default_value" + }, + "query": "default_value", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2beta1.json new file mode 100644 index 00000000000..3fdcd01a27c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2beta1.json @@ -0,0 +1,215 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v27.repeated_panels_and_constant_variable.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Normal Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Normal nested panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with repeated panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V27 Repeated Panels and Constant Variable Migration Test Dashboard", + "variables": [ + { + "kind": "TextVariable", + "spec": { + "name": "constant_var", + "current": { + "text": "default_value", + "value": "default_value" + }, + "query": "default_value", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v0alpha1.json new file mode 100644 index 00000000000..6a3a6ef7f8b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v0alpha1.json @@ -0,0 +1,97 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v28.remove_variable_properties.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "query_variable_with_tags", + "options": [], + "query": "label_values(up, instance)", + "refresh": 1, + "type": "query" + }, + { + "name": "custom_variable_with_tags", + "options": [ + { + "text": "Option 1", + "value": "opt1" + }, + { + "text": "Option 2", + "value": "opt2" + } + ], + "type": "custom", + "useTags": false + }, + { + "name": "clean_variable", + "options": [ + { + "text": "Hello", + "value": "World" + } + ], + "type": "textbox" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2alpha1.json new file mode 100644 index 00000000000..dd56c1cd995 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2alpha1.json @@ -0,0 +1,135 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v28.remove_variable_properties.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "query_variable_with_tags", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_variable_with_tags", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "Option 1", + "value": "opt1" + }, + { + "selected": false, + "text": "Option 2", + "value": "opt2" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "clean_variable", + "current": { + "text": "", + "value": "" + }, + "query": "", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2beta1.json new file mode 100644 index 00000000000..c938e4a730f --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2beta1.json @@ -0,0 +1,138 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v28.remove_variable_properties.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "query_variable_with_tags", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_variable_with_tags", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "Option 1", + "value": "opt1" + }, + { + "selected": false, + "text": "Option 2", + "value": "opt2" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "clean_variable", + "current": { + "text": "", + "value": "" + }, + "query": "", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v0alpha1.json new file mode 100644 index 00000000000..962c8041848 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v0alpha1.json @@ -0,0 +1,254 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v28.singlestat_and_variable_properties.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "max": 10, + "min": 1 + }, + "id": 1, + "legend": true, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B" + } + ], + "thresholds": "10,20,30", + "type": "stat" + }, + { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "thresholds": "10,20,30", + "type": "stat" + }, + { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "max": 10, + "min": 1 + }, + "id": 3, + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B" + } + ], + "thresholds": "10,20,30", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "rate(http_requests_total[5m])", + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "query_variable_with_tags", + "options": [], + "query": "label_values(up, instance)", + "refresh": 1, + "type": "query" + }, + { + "name": "custom_variable_with_tags", + "options": [ + { + "text": "Option 1", + "value": "opt1" + }, + { + "text": "Option 2", + "value": "opt2" + } + ], + "type": "custom", + "useTags": false + }, + { + "name": "clean_variable", + "options": [ + { + "text": "Hello", + "value": "World" + } + ], + "type": "textbox" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2alpha1.json new file mode 100644 index 00000000000..c819be43bc1 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2alpha1.json @@ -0,0 +1,397 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v28.singlestat_and_variable_properties.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "rate(http_requests_total[5m])" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "query_variable_with_tags", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_variable_with_tags", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "Option 1", + "value": "opt1" + }, + { + "selected": false, + "text": "Option 2", + "value": "opt2" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "clean_variable", + "current": { + "text": "", + "value": "" + }, + "query": "", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2beta1.json new file mode 100644 index 00000000000..5a0d8b3a96b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2beta1.json @@ -0,0 +1,410 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v28.singlestat_and_variable_properties.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "rate(http_requests_total[5m])" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "query_variable_with_tags", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_variable_with_tags", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "Option 1", + "value": "opt1" + }, + { + "selected": false, + "text": "Option 2", + "value": "opt2" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "clean_variable", + "current": { + "text": "", + "value": "" + }, + "query": "", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v0alpha1.json new file mode 100644 index 00000000000..b873e703936 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v0alpha1.json @@ -0,0 +1,442 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v28.singlestat_migration.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "max": 10, + "min": 1 + }, + "id": 1, + "legend": true, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B" + } + ], + "thresholds": "10,20,30", + "type": "stat" + }, + { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "max": 10, + "min": 1 + }, + "id": 2, + "legend": true, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B" + } + ], + "thresholds": "", + "type": "stat" + }, + { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gauge": { + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "grid": { + "max": 10, + "min": 1 + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "thresholds": "10,20,30", + "type": "stat" + }, + { + "autoMigrateFrom": "singlestat", + "colors": [ + "#FF0000", + "green", + "orange" + ], + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "grid": { + "max": 10, + "min": 1 + }, + "id": 4, + "legend": true, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B" + } + ], + "thresholds": "10,20,30", + "type": "stat", + "valueMaps": [ + { + "op": "=", + "text": "test", + "value": "20" + }, + { + "op": "=", + "text": "test1", + "value": "30" + }, + { + "op": "=", + "text": "50", + "value": "40" + } + ] + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 8, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "rate(http_requests_total[5m])", + "refId": "A" + } + ], + "type": "timeseries" + }, + { + "autoMigrateFrom": "grafana-singlestat-panel", + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": { + "type": "prometheus" + }, + "format": "areaF2", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 43 + }, + "id": 5, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "postfix": "b", + "postfixFontSize": "50%", + "prefix": "a", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PD8C576611E62080A" + }, + "refId": "A" + } + ], + "thresholds": "", + "title": "grafana-singlestat-panel", + "type": "stat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "datasource": { + "type": "prometheus" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 43 + }, + "id": 6, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "1.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PD8C576611E62080A" + }, + "refId": "A" + } + ], + "title": "singlestat (old, internal. Migrated if schema \u003c 28)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 43 + }, + "id": 7, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Singlestat \u003e\u003e Stat\n\nKnown issues:\n* limited options", + "mode": "markdown" + }, + "pluginVersion": "1.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PD8C576611E62080A" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2alpha1.json new file mode 100644 index 00000000000..985b316822d --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2alpha1.json @@ -0,0 +1,626 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v28.singlestat_migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "grafana-singlestat-panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "maxDataPoints": 100 + } + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "singlestat (old, internal. Migrated if schema \u003c 28)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "maxDataPoints": 100 + } + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "1.0.0", + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "unit": "ms", + "mappings": [ + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "N/A" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + } + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "1.0.0", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Singlestat \u003e\u003e Stat\n\nKnown issues:\n* limited options", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "rate(http_requests_total[5m])" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2beta1.json new file mode 100644 index 00000000000..2b7d27024c6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2beta1.json @@ -0,0 +1,646 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v28.singlestat_migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "grafana-singlestat-panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "PD8C576611E62080A" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "maxDataPoints": 100 + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "singlestat (old, internal. Migrated if schema \u003c 28)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "PD8C576611E62080A" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": { + "maxDataPoints": 100 + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "1.0.0", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "unit": "ms", + "mappings": [ + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "N/A" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + } + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Status + Notes", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "PD8C576611E62080A" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "1.0.0", + "spec": { + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Singlestat \u003e\u003e Stat\n\nKnown issues:\n* limited options", + "mode": "markdown" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "rate(http_requests_total[5m])" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 16, + "y": 43, + "width": 8, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V28 Singlestat and Variable Properties Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v0alpha1.json new file mode 100644 index 00000000000..9b05758f191 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v0alpha1.json @@ -0,0 +1,187 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v29.query_variables_refresh_and_options.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "uid": "prometheus" + }, + "id": 1, + "targets": [ + { + "datasource": { + "uid": "prometheus" + }, + "expr": "up", + "refId": "A" + } + ], + "title": "Test Panel", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "never_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "never_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "dashboard_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "dashboard_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "timerange_refresh_with_options", + "options": [], + "refresh": 2, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "timerange_refresh_without_options", + "options": [], + "refresh": 2, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "no_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "no_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "unknown_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": "prometheus", + "name": "unknown_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "name": "custom_variable", + "options": [ + { + "text": "custom", + "value": "custom" + } + ], + "type": "custom" + }, + { + "name": "textbox_variable", + "options": [ + { + "text": "Hello", + "value": "World" + } + ], + "type": "textbox" + }, + { + "name": "datasource_variable", + "options": [], + "type": "datasource" + }, + { + "name": "interval_variable", + "options": [ + { + "text": "1m", + "value": "1m" + } + ], + "type": "interval" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V29 Query Variables Refresh and Options Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2alpha1.json new file mode 100644 index 00000000000..ce4f5103c7d --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2alpha1.json @@ -0,0 +1,439 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v29.query_variables_refresh_and_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Test Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "up" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V29 Query Variables Refresh and Options Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "never_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "never_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "dashboard_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "dashboard_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "timerange_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "timerange_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "no_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "no_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "unknown_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "unknown_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_variable", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "custom", + "value": "custom" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "textbox_variable", + "current": { + "text": "", + "value": "" + }, + "query": "", + "hide": "dontHide", + "skipUrlSync": false + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource_variable", + "pluginId": "prometheus", + "refresh": "never", + "regex": "", + "current": { + "text": "", + "value": "" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval_variable", + "query": "1m", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + } + ], + "auto": false, + "auto_min": "", + "auto_count": 0, + "refresh": "onTimeRangeChanged", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2beta1.json new file mode 100644 index 00000000000..98d2361f0ae --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2beta1.json @@ -0,0 +1,462 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v29.query_variables_refresh_and_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Test Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus" + }, + "spec": { + "expr": "up" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V29 Query Variables Refresh and Options Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "never_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "never_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "dashboard_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "dashboard_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "timerange_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "timerange_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "no_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "no_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "unknown_refresh_with_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "unknown_refresh_without_options", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_variable", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "custom", + "value": "custom" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "textbox_variable", + "current": { + "text": "", + "value": "" + }, + "query": "", + "hide": "dontHide", + "skipUrlSync": false + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource_variable", + "pluginId": "prometheus", + "refresh": "never", + "regex": "", + "current": { + "text": "", + "value": "" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval_variable", + "query": "1m", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + } + ], + "auto": false, + "auto_min": "", + "auto_count": 0, + "refresh": "onTimeRangeChanged", + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v0alpha1.json new file mode 100644 index 00000000000..c18be5720dd --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v0alpha1.json @@ -0,0 +1,127 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v3.no-op.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "barchart" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "barchart" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V3 No-Op Migration - but tests ensuring panel IDs are unique", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2alpha1.json new file mode 100644 index 00000000000..4916dcaa820 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2alpha1.json @@ -0,0 +1,298 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v3.no-op.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V3 No-Op Migration - but tests ensuring panel IDs are unique", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2beta1.json new file mode 100644 index 00000000000..4a38725abc3 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2beta1.json @@ -0,0 +1,307 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v3.no-op.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V3 No-Op Migration - but tests ensuring panel IDs are unique", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v0alpha1.json new file mode 100644 index 00000000000..4c3176f47e9 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v0alpha1.json @@ -0,0 +1,400 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v30.value_mappings_and_tooltip_options.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "text": "Down" + }, + "1": { + "text": "Up" + } + }, + "type": "value" + }, + { + "options": { + "from": 10, + "result": { + "text": "Medium" + }, + "to": 20 + }, + "type": "range" + }, + { + "options": { + "match": "null", + "result": { + "text": "Null Value" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "test-field" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "text": "Override Up" + } + }, + "type": "value" + } + ] + } + ] + } + ] + }, + "id": 1, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with legacy value mappings and tooltip options", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "options": { + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "XY Chart with tooltip options only", + "type": "xychart" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "options": { + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "XY Chart2 with tooltip options", + "type": "xychart2" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "options": { + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph panel gets migrated to timeseries and tooltip", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "100": { + "text": "Critical" + } + }, + "type": "value" + }, + { + "options": { + "from": 50, + "result": { + "text": "Warning" + }, + "to": 99 + }, + "type": "range" + }, + { + "options": { + "from": 0, + "result": { + "text": "OK" + }, + "to": 49 + }, + "type": "range" + } + ] + }, + "overrides": [] + }, + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with complex value mappings", + "type": "stat" + }, + { + "collapsed": true, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 6, + "panels": [ + { + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "text": "Off" + }, + "1": { + "text": "On" + } + }, + "type": "value" + } + ] + } + }, + "id": 7, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "title": "Nested panel with both migrations", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Collapsed Row with nested panels", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "id": 8, + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with no relevant configurations", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "empty-field" + }, + "properties": [ + { + "id": "mappings", + "value": [] + } + ] + } + ] + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with empty mappings array - should return null", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2alpha1.json new file mode 100644 index 00000000000..5d81e9bab64 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2alpha1.json @@ -0,0 +1,693 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v30.value_mappings_and_tooltip_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with legacy value mappings and tooltip options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "tooltip": { + "mode": "multi" + } + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down" + }, + "1": { + "text": "Up" + } + } + }, + { + "type": "range", + "options": { + "from": 10, + "to": 20, + "result": { + "text": "Medium" + } + } + }, + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "Null Value" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "test-field" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "text": "Override Up" + } + }, + "type": "value" + } + ] + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "XY Chart with tooltip options only", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "xychart", + "spec": { + "pluginVersion": "", + "options": { + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "XY Chart2 with tooltip options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "xychart2", + "spec": { + "pluginVersion": "", + "options": { + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Graph panel gets migrated to timeseries and tooltip", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with complex value mappings", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "100": { + "text": "Critical" + } + } + }, + { + "type": "range", + "options": { + "from": 50, + "to": 99, + "result": { + "text": "Warning" + } + } + }, + { + "type": "range", + "options": { + "from": 0, + "to": 49, + "result": { + "text": "OK" + } + } + } + ] + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Nested panel with both migrations", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "tooltip": { + "mode": "multi" + } + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Off" + }, + "1": { + "text": "On" + } + } + } + ] + }, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with no relevant configurations", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with empty mappings array - should return null", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "empty-field" + }, + "properties": [ + { + "id": "mappings", + "value": [] + } + ] + } + ] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": true, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row with nested panels", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2beta1.json new file mode 100644 index 00000000000..6848d2d1714 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2beta1.json @@ -0,0 +1,709 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v30.value_mappings_and_tooltip_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with legacy value mappings and tooltip options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "tooltip": { + "mode": "multi" + } + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down" + }, + "1": { + "text": "Up" + } + } + }, + { + "type": "range", + "options": { + "from": 10, + "to": 20, + "result": { + "text": "Medium" + } + } + }, + { + "type": "special", + "options": { + "match": "null", + "result": { + "text": "Null Value" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "test-field" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "text": "Override Up" + } + }, + "type": "value" + } + ] + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "XY Chart with tooltip options only", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "xychart", + "version": "", + "spec": { + "options": { + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "XY Chart2 with tooltip options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "xychart2", + "version": "", + "spec": { + "options": { + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Graph panel gets migrated to timeseries and tooltip", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with complex value mappings", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "100": { + "text": "Critical" + } + } + }, + { + "type": "range", + "options": { + "from": 50, + "to": 99, + "result": { + "text": "Warning" + } + } + }, + { + "type": "range", + "options": { + "from": 0, + "to": 49, + "result": { + "text": "OK" + } + } + } + ] + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Nested panel with both migrations", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "tooltip": { + "mode": "multi" + } + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Off" + }, + "1": { + "text": "On" + } + } + } + ] + }, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with no relevant configurations", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with empty mappings array - should return null", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "empty-field" + }, + "properties": [ + { + "id": "mappings", + "value": [] + } + ] + } + ] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": true, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row with nested panels", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v0alpha1.json new file mode 100644 index 00000000000..9cbd5912975 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v0alpha1.json @@ -0,0 +1,304 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v31.labels_to_fields_merge.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with basic labelsToFields transformation", + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with labelsToFields options preserved", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "job", + "instance", + "region" + ], + "mode": "rows", + "valueLabel": "value" + } + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with multiple labelsToFields transformations", + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + }, + { + "id": "calculateField", + "options": {} + }, + { + "id": "labelsToFields", + "options": { + "mode": "rows" + } + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with no transformations", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with other transformations only", + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "reduce", + "options": {} + } + ], + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "panels": [ + { + "id": 6, + "title": "Nested panel with labelsToFields", + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "id": 7, + "title": "Nested panel without labelsToFields", + "transformations": [ + { + "id": "organize", + "options": {} + } + ], + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Row with nested panels", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 8, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with labelsToFields and existing merge", + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + }, + { + "id": "merge", + "options": {} + }, + { + "id": "reduce", + "options": {} + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V31 LabelsToFields Merge Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2alpha1.json new file mode 100644 index 00000000000..1ad2c6f1b29 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2alpha1.json @@ -0,0 +1,673 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v31.labels_to_fields_merge.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with basic labelsToFields transformation", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with multiple labelsToFields transformations", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "organize", + "spec": { + "id": "organize", + "options": {} + } + }, + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + }, + { + "kind": "calculateField", + "spec": { + "id": "calculateField", + "options": {} + } + }, + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": { + "mode": "rows" + } + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with no transformations", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with other transformations only", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "organize", + "spec": { + "id": "organize", + "options": {} + } + }, + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Nested panel with labelsToFields", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Nested panel without labelsToFields", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "organize", + "spec": { + "id": "organize", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with labelsToFields and existing merge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + }, + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with labelsToFields options preserved", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "job", + "instance", + "region" + ], + "mode": "rows", + "valueLabel": "value" + } + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with nested panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V31 LabelsToFields Merge Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2beta1.json new file mode 100644 index 00000000000..d7f0684597c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2beta1.json @@ -0,0 +1,688 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v31.labels_to_fields_merge.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with basic labelsToFields transformation", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with multiple labelsToFields transformations", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "organize", + "spec": { + "id": "organize", + "options": {} + } + }, + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + }, + { + "kind": "calculateField", + "spec": { + "id": "calculateField", + "options": {} + } + }, + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": { + "mode": "rows" + } + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with no transformations", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with other transformations only", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "organize", + "spec": { + "id": "organize", + "options": {} + } + }, + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Nested panel with labelsToFields", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Nested panel without labelsToFields", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "organize", + "spec": { + "id": "organize", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with labelsToFields and existing merge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + }, + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with labelsToFields options preserved", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "job", + "instance", + "region" + ], + "mode": "rows", + "valueLabel": "value" + } + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with nested panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V31 LabelsToFields Merge Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v0alpha1.json new file mode 100644 index 00000000000..7d612b2ed67 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v0alpha1.json @@ -0,0 +1,173 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v32.no_op_migration.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "uid": "grafana" + }, + "enable": true, + "name": "Deployments" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with transformations remains unchanged", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "job", + "instance" + ], + "mode": "rows" + } + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph panel remains unchanged", + "type": "timeseries", + "yAxes": [ + { + "show": true + } + ] + }, + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "panels": [ + { + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "id": 4, + "title": "Nested stat panel", + "type": "stat" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Row with nested panels", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": "prometheus", + "name": "environment", + "options": [], + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "V32 No-Op Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2alpha1.json new file mode 100644 index 00000000000..1d68e967a49 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2alpha1.json @@ -0,0 +1,314 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v32.no_op_migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "", + "name": "Deployments" + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with transformations remains unchanged", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "job", + "instance" + ], + "mode": "rows" + } + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Graph panel remains unchanged", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Nested stat panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with nested panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V32 No-Op Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "environment", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2beta1.json new file mode 100644 index 00000000000..b80ef22e794 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2beta1.json @@ -0,0 +1,324 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v32.no_op_migration.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "", + "name": "Deployments" + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with transformations remains unchanged", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "labelsToFields", + "spec": { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "job", + "instance" + ], + "mode": "rows" + } + } + }, + { + "kind": "merge", + "spec": { + "id": "merge", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Graph panel remains unchanged", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Nested stat panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with nested panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V32 No-Op Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "environment", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v0alpha1.json new file mode 100644 index 00000000000..a178fe73218 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v0alpha1.json @@ -0,0 +1,307 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v33.panel_ds_name_to_ref.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "description": "Tests v33 migration behavior when panel datasource is explicitly null. Should remain null after migration (returnDefaultAsNull: true).", + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "description": "Target with UID reference should migrate to full object", + "refId": "A" + } + ], + "title": "Panel Datasource: null → should stay null", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "description": "Tests v33 migration behavior when panel datasource is already a proper object reference. Should remain unchanged.", + "id": 2, + "targets": [ + { + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "description": "Target with existing object should remain unchanged", + "refId": "A" + } + ], + "title": "Panel Datasource: existing object → should stay unchanged", + "type": "stat" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "description": "Tests v33 migration when panel datasource is a string name. Should convert to proper object with uid, type, apiVersion.", + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel Datasource: string name → should migrate to object", + "type": "table" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests v33 migration when panel has datasource string but empty targets array. Panel datasource should still migrate.", + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel Datasource: string name with empty targets → should migrate", + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "description": "Tests v33 target migration with various edge cases: null target (unchanged), valid string (migrated), non-existing string (preserved), missing datasource field (unchanged).", + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "description": "Null target datasource should remain null", + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Valid string should migrate to object", + "refId": "B" + }, + { + "datasource": { + "uid": "non-existing-ds" + }, + "description": "Non-existing datasource should be preserved as-is (migration returns nil)", + "refId": "C" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "description": "Target without datasource field should remain unchanged", + "refId": "D" + } + ], + "title": "Target Datasources: mixed null/string/non-existing scenarios", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests v33 migration when panel datasource is null but targets have mixed reference types (object, string). Panel should stay null, targets should migrate appropriately.", + "id": 6, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref" + }, + "description": "Existing object target should remain unchanged", + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "description": "String target should migrate to object", + "refId": "B" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Default datasource string should migrate to object", + "refId": "C" + } + ], + "title": "Panel: null datasource with mixed target types", + "type": "timeseries" + }, + { + "datasource": {}, + "description": "Tests v33 migration behavior with empty string datasource. Should migrate to empty object {} based on MigrateDatasourceNameToRef logic.", + "id": 7, + "targets": [ + { + "datasource": {}, + "description": "Empty string target should also migrate to empty object {}", + "refId": "A" + } + ], + "title": "Empty string datasource → should return empty object {}", + "type": "stat" + }, + { + "datasource": { + "uid": "completely-missing-ds" + }, + "description": "Tests v33 migration with completely unknown datasource names. Since migration returns nil for unknown datasources, they should be preserved unchanged.", + "id": 8, + "targets": [ + { + "datasource": { + "uid": "also-missing-ds" + }, + "description": "Unknown target datasource should remain unchanged (migration returns nil)", + "refId": "A" + }, + { + "datasource": { + "uid": "completely-missing-ds" + }, + "description": "Empty string target should migrate to {}", + "refId": "B" + } + ], + "title": "Non-existing datasources → should be preserved as-is", + "type": "table" + }, + { + "collapsed": true, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests v33 migration handles nested panels within collapsed rows. Nested panel datasources should migrate same as top-level panels.", + "id": 9, + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "description": "Nested panel with string datasource should migrate to proper object reference, proving row panel recursion works.", + "id": 10, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Nested target should also migrate from string to object", + "refId": "A" + } + ], + "title": "Nested Panel: string datasource → should migrate to object", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Row Panel: nested panels should also migrate", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V33 Panel Datasource Name to Ref Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2alpha1.json new file mode 100644 index 00000000000..c38887d458c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2alpha1.json @@ -0,0 +1,724 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v33.panel_ds_name_to_ref.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel Datasource: null → should stay null", + "description": "Tests v33 migration behavior when panel datasource is explicitly null. Should remain null after migration (returnDefaultAsNull: true).", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Target with UID reference should migrate to full object" + } + }, + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Nested Panel: string datasource → should migrate to object", + "description": "Nested panel with string datasource should migrate to proper object reference, proving row panel recursion works.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Nested target should also migrate from string to object" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel Datasource: existing object → should stay unchanged", + "description": "Tests v33 migration behavior when panel datasource is already a proper object reference. Should remain unchanged.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Target with existing object should remain unchanged" + } + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel Datasource: string name → should migrate to object", + "description": "Tests v33 migration when panel datasource is a string name. Should convert to proper object with uid, type, apiVersion.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel Datasource: string name with empty targets → should migrate", + "description": "Tests v33 migration when panel has datasource string but empty targets array. Panel datasource should still migrate.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Target Datasources: mixed null/string/non-existing scenarios", + "description": "Tests v33 target migration with various edge cases: null target (unchanged), valid string (migrated), non-existing string (preserved), missing datasource field (unchanged).", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Null target datasource should remain null" + } + }, + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Valid string should migrate to object" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Non-existing datasource should be preserved as-is (migration returns nil)" + } + }, + "datasource": { + "type": "prometheus", + "uid": "non-existing-ds" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Target without datasource field should remain unchanged" + } + }, + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel: null datasource with mixed target types", + "description": "Tests v33 migration when panel datasource is null but targets have mixed reference types (object, string). Panel should stay null, targets should migrate appropriately.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Existing object target should remain unchanged" + } + }, + "datasource": { + "type": "prometheus", + "uid": "existing-ref" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "String target should migrate to object" + } + }, + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Default datasource string should migrate to object" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "C", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Empty string datasource → should return empty object {}", + "description": "Tests v33 migration behavior with empty string datasource. Should migrate to empty object {} based on MigrateDatasourceNameToRef logic.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Empty string target should also migrate to empty object {}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Non-existing datasources → should be preserved as-is", + "description": "Tests v33 migration with completely unknown datasource names. Since migration returns nil for unknown datasources, they should be preserved unchanged.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Unknown target datasource should remain unchanged (migration returns nil)" + } + }, + "datasource": { + "type": "prometheus", + "uid": "also-missing-ds" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "description": "Empty string target should migrate to {}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "completely-missing-ds" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": true, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row Panel: nested panels should also migrate", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V33 Panel Datasource Name to Ref Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2beta1.json new file mode 100644 index 00000000000..fbb8d83b728 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2beta1.json @@ -0,0 +1,750 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v33.panel_ds_name_to_ref.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel Datasource: null → should stay null", + "description": "Tests v33 migration behavior when panel datasource is explicitly null. Should remain null after migration (returnDefaultAsNull: true).", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "non-default-test-ds-uid" + }, + "spec": { + "description": "Target with UID reference should migrate to full object" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Nested Panel: string datasource → should migrate to object", + "description": "Nested panel with string datasource should migrate to proper object reference, proving row panel recursion works.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "description": "Nested target should also migrate from string to object" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel Datasource: existing object → should stay unchanged", + "description": "Tests v33 migration behavior when panel datasource is already a proper object reference. Should remain unchanged.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": { + "description": "Target with existing object should remain unchanged" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel Datasource: string name → should migrate to object", + "description": "Tests v33 migration when panel datasource is a string name. Should convert to proper object with uid, type, apiVersion.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "non-default-test-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel Datasource: string name with empty targets → should migrate", + "description": "Tests v33 migration when panel has datasource string but empty targets array. Panel datasource should still migrate.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Target Datasources: mixed null/string/non-existing scenarios", + "description": "Tests v33 target migration with various edge cases: null target (unchanged), valid string (migrated), non-existing string (preserved), missing datasource field (unchanged).", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "non-default-test-ds-uid" + }, + "spec": { + "description": "Null target datasource should remain null" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "description": "Valid string should migrate to object" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "non-existing-ds" + }, + "spec": { + "description": "Non-existing datasource should be preserved as-is (migration returns nil)" + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "non-default-test-ds-uid" + }, + "spec": { + "description": "Target without datasource field should remain unchanged" + } + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel: null datasource with mixed target types", + "description": "Tests v33 migration when panel datasource is null but targets have mixed reference types (object, string). Panel should stay null, targets should migrate appropriately.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "existing-ref" + }, + "spec": { + "description": "Existing object target should remain unchanged" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "non-default-test-ds-uid" + }, + "spec": { + "description": "String target should migrate to object" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "description": "Default datasource string should migrate to object" + } + }, + "refId": "C", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Empty string datasource → should return empty object {}", + "description": "Tests v33 migration behavior with empty string datasource. Should migrate to empty object {} based on MigrateDatasourceNameToRef logic.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "description": "Empty string target should also migrate to empty object {}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Non-existing datasources → should be preserved as-is", + "description": "Tests v33 migration with completely unknown datasource names. Since migration returns nil for unknown datasources, they should be preserved unchanged.", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "also-missing-ds" + }, + "spec": { + "description": "Unknown target datasource should remain unchanged (migration returns nil)" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "completely-missing-ds" + }, + "spec": { + "description": "Empty string target should migrate to {}" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": true, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row Panel: nested panels should also migrate", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V33 Panel Datasource Name to Ref Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v0alpha1.json new file mode 100644 index 00000000000..a8caa269744 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v0alpha1.json @@ -0,0 +1,1045 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v34.multiple_stats_cloudwatch.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-123456" + }, + "enable": true, + "iconColor": "red", + "name": "CloudWatch Annotation Single Statistic", + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-789012" + }, + "enable": true, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Maximum", + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Maximum" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "LoadBalancer": "my-lb" + }, + "enable": true, + "iconColor": "green", + "name": "CloudWatch Annotation Empty Statistics", + "namespace": "AWS/ApplicationELB", + "prefixMatching": false, + "region": "us-west-1" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "TableName": "my-table" + }, + "enable": true, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - InvalidStat", + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "InvalidStat" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "enable": true, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - null", + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "enable": true, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - 123", + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": 123 + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "enable": true, + "iconColor": "purple", + "name": "Non-CloudWatch Annotation" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-789012" + }, + "enable": true, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Minimum", + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Minimum" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-789012" + }, + "enable": true, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Sum", + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Sum" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "TableName": "my-table" + }, + "enable": true, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - Sum", + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Sum" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "TableName": "my-table" + }, + "enable": true, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - null", + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "TableName": "my-table" + }, + "enable": true, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - Average", + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "enable": true, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - Average", + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "enable": true, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - ", + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "enable": true, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - true", + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": true + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "enable": true, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - [object Object]", + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": {} + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "refId": "A", + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "refId": "B", + "region": "us-east-1", + "statistic": "Maximum" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "refId": "C", + "region": "us-east-1", + "statistic": "Minimum" + } + ], + "title": "CloudWatch Single Query Multiple Statistics", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "LoadBalancer": "my-load-balancer" + }, + "metricEditorMode": 0, + "metricName": "RequestCount", + "metricQueryType": 0, + "namespace": "AWS/ApplicationELB", + "refId": "A", + "region": "us-west-2", + "statistic": "Sum" + } + ], + "title": "CloudWatch Single Query Single Statistic", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "DBInstanceIdentifier": "my-db" + }, + "metricEditorMode": 0, + "metricName": "DatabaseConnections", + "metricQueryType": 0, + "namespace": "AWS/RDS", + "refId": "A", + "region": "us-east-1", + "statistic": "Maximum" + } + ], + "title": "CloudWatch Query No Statistics Array", + "type": "timeseries" + }, + { + "datasource": { + "uid": "prometheus" + }, + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "QueueName": "my-queue" + }, + "metricEditorMode": 0, + "metricName": "ApproximateNumberOfMessages", + "metricQueryType": 0, + "namespace": "AWS/SQS", + "refId": "A", + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "uid": "prometheus" + }, + "expr": "up", + "refId": "B" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "TopicName": "my-topic" + }, + "metricEditorMode": 0, + "metricName": "NumberOfMessagesPublished", + "metricQueryType": 0, + "namespace": "AWS/SNS", + "refId": "C", + "region": "us-west-1", + "statistic": "Sum" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "QueueName": "my-queue" + }, + "metricEditorMode": 0, + "metricName": "ApproximateNumberOfMessages", + "metricQueryType": 0, + "namespace": "AWS/SQS", + "refId": "D", + "region": "us-east-1", + "statistic": "Maximum" + } + ], + "title": "Mixed CloudWatch and Non-CloudWatch Queries", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "BucketName": "my-bucket" + }, + "metricEditorMode": 0, + "metricName": "BucketSizeBytes", + "metricQueryType": 0, + "namespace": "AWS/S3", + "refId": "A", + "region": "us-east-1" + } + ], + "title": "CloudWatch Query Empty Statistics", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 6, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "refId": "A", + "region": "us-west-2", + "statistic": "InvalidStat" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "refId": "B", + "region": "us-west-2", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "refId": "C", + "region": "us-west-2" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "refId": "D", + "region": "us-west-2", + "statistic": "Maximum" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "refId": "E", + "region": "us-west-2", + "statistic": "" + } + ], + "title": "CloudWatch Query Invalid Statistics", + "type": "timeseries" + }, + { + "collapsed": true, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 7, + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 8, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "refId": "A", + "region": "us-east-1", + "statistic": "Sum" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "refId": "B", + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "refId": "C", + "region": "us-east-1", + "statistic": "Maximum" + } + ], + "title": "Nested CloudWatch Query Multiple Statistics", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Collapsed Row with CloudWatch", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "ClusterName": "my-cluster" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/ECS", + "period": "300", + "refId": "A", + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "ClusterName": "my-cluster" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/ECS", + "period": "300", + "refId": "B", + "region": "us-east-1", + "statistic": "Maximum" + } + ], + "title": "CloudWatch Query with Existing Editor Mode", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 10, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-missing-fields" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "A", + "region": "us-east-1", + "statistic": "Average" + } + ], + "title": "CloudWatch Query Missing Editor Fields", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 11, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-with-expression" + }, + "expression": "SEARCH('{AWS/EC2,InstanceId} MetricName=\"CPUUtilization\"', 'Average', 300)", + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "A", + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-with-expression" + }, + "expression": "SEARCH('{AWS/EC2,InstanceId} MetricName=\"CPUUtilization\"', 'Average', 300)", + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "B", + "region": "us-east-1", + "statistic": "Maximum" + } + ], + "title": "CloudWatch Query with Expression (Code Mode)", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 12, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-insights" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/EC2", + "refId": "A", + "region": "us-east-1", + "statistic": "Average" + } + ], + "title": "CloudWatch Insights Query Missing Editor Mode", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 13, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "A", + "region": "us-east-1" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "B", + "region": "us-east-1", + "statistic": "Average" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "C", + "region": "us-east-1", + "statistic": "" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "D", + "region": "us-east-1", + "statistic": "Maximum" + } + ], + "title": "CloudWatch Query with Null Statistics", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 14, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "A", + "region": "us-east-1", + "statistic": 123 + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "B", + "region": "us-east-1", + "statistic": true + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "C", + "region": "us-east-1", + "statistic": {} + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "refId": "D", + "region": "us-east-1", + "statistic": [] + } + ], + "title": "CloudWatch Query with Only Invalid Statistics", + "type": "timeseries" + }, + { + "datasource": { + "uid": "prometheus" + }, + "id": 15, + "targets": [ + { + "datasource": { + "uid": "prometheus" + }, + "expr": "cpu_usage", + "refId": "A" + } + ], + "title": "Non-CloudWatch Panel", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "CloudWatch Multiple Statistics Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2alpha1.json new file mode 100644 index 00000000000..963c1e4b649 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2alpha1.json @@ -0,0 +1,1904 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v34.multiple_stats_cloudwatch.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "CloudWatch Annotation Single Statistic", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-123456" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Maximum", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-789012" + }, + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Maximum" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "green", + "name": "CloudWatch Annotation Empty Statistics", + "legacyOptions": { + "dimensions": { + "LoadBalancer": "my-lb" + }, + "namespace": "AWS/ApplicationELB", + "prefixMatching": false, + "region": "us-west-1" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - InvalidStat", + "legacyOptions": { + "dimensions": { + "TableName": "my-table" + }, + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "InvalidStat" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - null", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - 123", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": 123 + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "purple", + "name": "Non-CloudWatch Annotation" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Minimum", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-789012" + }, + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Minimum" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Sum", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-789012" + }, + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Sum" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - Sum", + "legacyOptions": { + "dimensions": { + "TableName": "my-table" + }, + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Sum" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - null", + "legacyOptions": { + "dimensions": { + "TableName": "my-table" + }, + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - Average", + "legacyOptions": { + "dimensions": { + "TableName": "my-table" + }, + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - Average", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - ", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - true", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": true + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - [object Object]", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": {} + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CloudWatch Single Query Multiple Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "region": "us-east-1", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "region": "us-east-1", + "statistic": "Minimum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "C", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "CloudWatch Query Missing Editor Fields", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-missing-fields" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "CloudWatch Query with Expression (Code Mode)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-with-expression" + }, + "expression": "SEARCH('{AWS/EC2,InstanceId} MetricName=\"CPUUtilization\"', 'Average', 300)", + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-with-expression" + }, + "expression": "SEARCH('{AWS/EC2,InstanceId} MetricName=\"CPUUtilization\"', 'Average', 300)", + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "CloudWatch Insights Query Missing Editor Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-insights" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "CloudWatch Query with Null Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-14": { + "kind": "Panel", + "spec": { + "id": 14, + "title": "CloudWatch Query with Only Invalid Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": 123 + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": true + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": {} + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": [] + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-15": { + "kind": "Panel", + "spec": { + "id": 15, + "title": "Non-CloudWatch Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "cpu_usage" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "CloudWatch Single Query Single Statistic", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "LoadBalancer": "my-load-balancer" + }, + "metricEditorMode": 0, + "metricName": "RequestCount", + "metricQueryType": 0, + "namespace": "AWS/ApplicationELB", + "region": "us-west-2", + "statistic": "Sum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "CloudWatch Query No Statistics Array", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "DBInstanceIdentifier": "my-db" + }, + "metricEditorMode": 0, + "metricName": "DatabaseConnections", + "metricQueryType": 0, + "namespace": "AWS/RDS", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Mixed CloudWatch and Non-CloudWatch Queries", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "QueueName": "my-queue" + }, + "metricEditorMode": 0, + "metricName": "ApproximateNumberOfMessages", + "metricQueryType": 0, + "namespace": "AWS/SQS", + "region": "us-east-1", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "up" + } + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "TopicName": "my-topic" + }, + "metricEditorMode": 0, + "metricName": "NumberOfMessagesPublished", + "metricQueryType": 0, + "namespace": "AWS/SNS", + "region": "us-west-1", + "statistic": "Sum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "QueueName": "my-queue" + }, + "metricEditorMode": 0, + "metricName": "ApproximateNumberOfMessages", + "metricQueryType": 0, + "namespace": "AWS/SQS", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "CloudWatch Query Empty Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "BucketName": "my-bucket" + }, + "metricEditorMode": 0, + "metricName": "BucketSizeBytes", + "metricQueryType": 0, + "namespace": "AWS/S3", + "region": "us-east-1" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "CloudWatch Query Invalid Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2", + "statistic": "InvalidStat" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2", + "statistic": "Maximum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "D", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2", + "statistic": "" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "E", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Nested CloudWatch Query Multiple Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "region": "us-east-1", + "statistic": "Sum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "region": "us-east-1", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "C", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "CloudWatch Query with Existing Editor Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "ClusterName": "my-cluster" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/ECS", + "period": "300", + "region": "us-east-1", + "statistic": "Average" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "ClusterName": "my-cluster" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/ECS", + "period": "300", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": true, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row with CloudWatch", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-14" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-15" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "CloudWatch Multiple Statistics Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2beta1.json new file mode 100644 index 00000000000..79ee3db369e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2beta1.json @@ -0,0 +1,1968 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v34.multiple_stats_cloudwatch.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "CloudWatch Annotation Single Statistic", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-123456" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Maximum", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-789012" + }, + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Maximum" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "green", + "name": "CloudWatch Annotation Empty Statistics", + "legacyOptions": { + "dimensions": { + "LoadBalancer": "my-lb" + }, + "namespace": "AWS/ApplicationELB", + "prefixMatching": false, + "region": "us-west-1" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - InvalidStat", + "legacyOptions": { + "dimensions": { + "TableName": "my-table" + }, + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "InvalidStat" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - null", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - 123", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": 123 + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "purple", + "name": "Non-CloudWatch Annotation" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Minimum", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-789012" + }, + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Minimum" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "CloudWatch Annotation Multiple Statistics - Sum", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-789012" + }, + "namespace": "AWS/RDS", + "prefixMatching": false, + "region": "us-west-2", + "statistic": "Sum" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - Sum", + "legacyOptions": { + "dimensions": { + "TableName": "my-table" + }, + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Sum" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - null", + "legacyOptions": { + "dimensions": { + "TableName": "my-table" + }, + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "CloudWatch Annotation Invalid Statistics - Average", + "legacyOptions": { + "dimensions": { + "TableName": "my-table" + }, + "namespace": "AWS/DynamoDB", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - Average", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "Average" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotation with Null in Statistics - ", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-null-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": "" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - true", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": true + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "pink", + "name": "CloudWatch Annotation Only Invalid Statistics - [object Object]", + "legacyOptions": { + "dimensions": { + "InstanceId": "i-invalid-annotation" + }, + "namespace": "AWS/EC2", + "prefixMatching": false, + "region": "us-east-1", + "statistic": {} + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CloudWatch Single Query Multiple Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "region": "us-east-1", + "statistic": "Average" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-123456" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "period": "300", + "region": "us-east-1", + "statistic": "Minimum" + } + }, + "refId": "C", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "CloudWatch Query Missing Editor Fields", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-missing-fields" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Average" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "CloudWatch Query with Expression (Code Mode)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-with-expression" + }, + "expression": "SEARCH('{AWS/EC2,InstanceId} MetricName=\"CPUUtilization\"', 'Average', 300)", + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Average" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-with-expression" + }, + "expression": "SEARCH('{AWS/EC2,InstanceId} MetricName=\"CPUUtilization\"', 'Average', 300)", + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "CloudWatch Insights Query Missing Editor Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-insights" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Average" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "CloudWatch Query with Null Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Average" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "" + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-null-stats" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-14": { + "kind": "Panel", + "spec": { + "id": 14, + "title": "CloudWatch Query with Only Invalid Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": 123 + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": true + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": {} + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-invalid-only" + }, + "metricEditorMode": 0, + "metricName": "CPUUtilization", + "metricQueryType": 0, + "namespace": "AWS/EC2", + "region": "us-east-1", + "statistic": [] + } + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-15": { + "kind": "Panel", + "spec": { + "id": 15, + "title": "Non-CloudWatch Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus" + }, + "spec": { + "expr": "cpu_usage" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "CloudWatch Single Query Single Statistic", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "LoadBalancer": "my-load-balancer" + }, + "metricEditorMode": 0, + "metricName": "RequestCount", + "metricQueryType": 0, + "namespace": "AWS/ApplicationELB", + "region": "us-west-2", + "statistic": "Sum" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "CloudWatch Query No Statistics Array", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "DBInstanceIdentifier": "my-db" + }, + "metricEditorMode": 0, + "metricName": "DatabaseConnections", + "metricQueryType": 0, + "namespace": "AWS/RDS", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Mixed CloudWatch and Non-CloudWatch Queries", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "QueueName": "my-queue" + }, + "metricEditorMode": 0, + "metricName": "ApproximateNumberOfMessages", + "metricQueryType": 0, + "namespace": "AWS/SQS", + "region": "us-east-1", + "statistic": "Average" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "prometheus" + }, + "spec": { + "expr": "up" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "TopicName": "my-topic" + }, + "metricEditorMode": 0, + "metricName": "NumberOfMessagesPublished", + "metricQueryType": 0, + "namespace": "AWS/SNS", + "region": "us-west-1", + "statistic": "Sum" + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "QueueName": "my-queue" + }, + "metricEditorMode": 0, + "metricName": "ApproximateNumberOfMessages", + "metricQueryType": 0, + "namespace": "AWS/SQS", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "refId": "D", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "CloudWatch Query Empty Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "BucketName": "my-bucket" + }, + "metricEditorMode": 0, + "metricName": "BucketSizeBytes", + "metricQueryType": 0, + "namespace": "AWS/S3", + "region": "us-east-1" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "CloudWatch Query Invalid Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2", + "statistic": "InvalidStat" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2", + "statistic": "Average" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2" + } + }, + "refId": "C", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2", + "statistic": "Maximum" + } + }, + "refId": "D", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "FunctionName": "my-function" + }, + "metricEditorMode": 0, + "metricName": "Duration", + "metricQueryType": 0, + "namespace": "AWS/Lambda", + "region": "us-west-2", + "statistic": "" + } + }, + "refId": "E", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Nested CloudWatch Query Multiple Statistics", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "region": "us-east-1", + "statistic": "Sum" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "region": "us-east-1", + "statistic": "Average" + } + }, + "refId": "B", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "StreamName": "my-stream" + }, + "metricEditorMode": 0, + "metricName": "IncomingRecords", + "metricQueryType": 0, + "namespace": "AWS/Kinesis", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "refId": "C", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "CloudWatch Query with Existing Editor Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "ClusterName": "my-cluster" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/ECS", + "period": "300", + "region": "us-east-1", + "statistic": "Average" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "dimensions": { + "ClusterName": "my-cluster" + }, + "metricEditorMode": 1, + "metricName": "CPUUtilization", + "metricQueryType": 1, + "namespace": "AWS/ECS", + "period": "300", + "region": "us-east-1", + "statistic": "Maximum" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": true, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row with CloudWatch", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-14" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-15" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "CloudWatch Multiple Statistics Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v0alpha1.json new file mode 100644 index 00000000000..747581ee194 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v0alpha1.json @@ -0,0 +1,308 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v35.ensure_x_axis_visibility.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + }, + "id": 6, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Timeseries with Hidden Axis", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Series A" + }, + "properties": [ + { + "id": "color.mode", + "value": "palette-classic" + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + }, + "id": 7, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Timeseries with Hidden Axis and Existing Overrides", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "auto" + } + }, + "overrides": [] + }, + "id": 8, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Timeseries with Auto Axis (No Change Expected)", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [] + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Stat Panel with Hidden Axis (No Change Expected)", + "type": "stat" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Timeseries with Missing FieldConfig", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 10, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Timeseries with Missing Defaults", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "id": 11, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Timeseries with Missing Custom Config", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + }, + "id": 12, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Timeseries with Missing Overrides Array", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "X-Axis Visibility Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2alpha1.json new file mode 100644 index 00000000000..936aeb8c30f --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2alpha1.json @@ -0,0 +1,599 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v35.ensure_x_axis_visibility.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Timeseries with Missing Defaults", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Timeseries with Missing Custom Config", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Timeseries with Missing Overrides Array", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Timeseries with Missing FieldConfig", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Timeseries with Hidden Axis", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Timeseries with Hidden Axis and Existing Overrides", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Series A" + }, + "properties": [ + { + "id": "color.mode", + "value": "palette-classic" + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Timeseries with Auto Axis (No Change Expected)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "auto" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Stat Panel with Hidden Axis (No Change Expected)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "X-Axis Visibility Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2beta1.json new file mode 100644 index 00000000000..f9be2a6cee1 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2beta1.json @@ -0,0 +1,616 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v35.ensure_x_axis_visibility.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Timeseries with Missing Defaults", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Timeseries with Missing Custom Config", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Timeseries with Missing Overrides Array", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Timeseries with Missing FieldConfig", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Timeseries with Hidden Axis", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Timeseries with Hidden Axis and Existing Overrides", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Series A" + }, + "properties": [ + { + "id": "color.mode", + "value": "palette-classic" + } + ] + }, + { + "matcher": { + "id": "byType", + "options": "time" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "auto" + } + ] + } + ] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Timeseries with Auto Axis (No Change Expected)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "auto" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Stat Panel with Hidden Axis (No Change Expected)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "axisPlacement": "hidden" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "X-Axis Visibility Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v0alpha1.json new file mode 100644 index 00000000000..a5dd8e99a9b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v0alpha1.json @@ -0,0 +1,427 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v36.ds_name_to_ref.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "name": "Default Annotation - Tests default datasource migration" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "name": "Named Datasource Annotation - Tests migration by datasource name" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "name": "UID Datasource Annotation - Tests migration by datasource UID" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "name": "Null Datasource Annotation - Tests null datasource fallback to default" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "unknown-datasource-name" + }, + "name": "Unknown Datasource Annotation - Tests unknown datasource preserved as UID" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests null panel datasource migration with targets - should fallback to default", + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with Null Datasource and Targets" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests null panel datasource with empty targets array - should create default target", + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with Null Datasource and Empty Targets" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests null panel datasource with missing targets - should create default target array", + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with No Targets Array" + }, + { + "datasource": { + "uid": "-- Mixed --" + }, + "description": "Tests mixed datasource panel - targets should migrate independently", + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "B" + } + ], + "title": "Panel with Mixed Datasources" + }, + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "description": "Tests panel with already migrated datasource object - should preserve existing refs", + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A" + } + ], + "title": "Panel with Existing Object Datasource" + }, + { + "datasource": { + "uid": "unknown-panel-datasource" + }, + "description": "Tests panel with unknown datasource - should preserve as UID-only reference", + "id": 6, + "targets": [ + { + "datasource": { + "uid": "unknown-target-datasource" + }, + "refId": "A" + } + ], + "title": "Panel with Unknown Datasource Name" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "description": "Tests panel with expression query - should not inherit expression as panel datasource", + "id": 7, + "targets": [ + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A" + }, + { + "datasource": { + "type": "__expr__", + "uid": "__expr__" + }, + "refId": "B" + } + ], + "title": "Panel with Expression Query" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "description": "Tests panel inheriting datasource from target when panel datasource was default", + "id": 8, + "targets": [ + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A" + } + ], + "title": "Panel Inheriting from Target" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "description": "Tests panel with datasource referenced by name - should migrate to full object", + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A" + } + ], + "title": "Panel with Named Datasource" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "description": "Tests panel with datasource referenced by UID - should migrate to full object", + "id": 10, + "targets": [ + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A" + } + ], + "title": "Panel with UID Datasource" + }, + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests row panel - it gets datasource or targets fields added even it is not needed, but this is how it works in frontend", + "id": 11, + "panels": [], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Simple Row Panel", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "description": "Tests collapsed row with nested panels - nested panels should migrate", + "id": 12, + "panels": [ + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "description": "Nested panel in collapsed row with default datasource", + "id": 13, + "targets": [ + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A" + } + ], + "title": "Nested Panel with Default Datasource" + }, + { + "datasource": { + "uid": "unknown-nested-datasource" + }, + "description": "Nested panel in collapsed row with unknown datasource", + "id": 14, + "targets": [ + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A" + } + ], + "title": "Nested Panel with Unknown Datasource" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Collapsed Row with Nested Panels", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "name": "query_var_null", + "options": [], + "type": "query" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "name": "query_var_named", + "options": [], + "type": "query" + }, + { + "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "name": "query_var_uid", + "options": [], + "type": "query" + }, + { + "datasource": { + "uid": "unknown-datasource" + }, + "name": "query_var_unknown", + "options": [], + "type": "query" + }, + { + "name": "non_query_var", + "type": "constant" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Datasource Reference Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2alpha1.json new file mode 100644 index 00000000000..17a351eaa46 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2alpha1.json @@ -0,0 +1,1033 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v36.ds_name_to_ref.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "Default Annotation - Tests default datasource migration" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "Named Datasource Annotation - Tests migration by datasource name" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "UID Datasource Annotation - Tests migration by datasource UID" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "Null Datasource Annotation - Tests null datasource fallback to default" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "unknown-datasource-name" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "Unknown Datasource Annotation - Tests unknown datasource preserved as UID" + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Null Datasource and Targets", + "description": "Tests null panel datasource migration with targets - should fallback to default", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Panel with UID Datasource", + "description": "Tests panel with datasource referenced by UID - should migrate to full object", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "Nested Panel with Default Datasource", + "description": "Nested panel in collapsed row with default datasource", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-14": { + "kind": "Panel", + "spec": { + "id": 14, + "title": "Nested Panel with Unknown Datasource", + "description": "Nested panel in collapsed row with unknown datasource", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with Null Datasource and Empty Targets", + "description": "Tests null panel datasource with empty targets array - should create default target", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with No Targets Array", + "description": "Tests null panel datasource with missing targets - should create default target array", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with Mixed Datasources", + "description": "Tests mixed datasource panel - targets should migrate independently", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with Existing Object Datasource", + "description": "Tests panel with already migrated datasource object - should preserve existing refs", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with Unknown Datasource Name", + "description": "Tests panel with unknown datasource - should preserve as UID-only reference", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "unknown-target-datasource" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel with Expression Query", + "description": "Tests panel with expression query - should not inherit expression as panel datasource", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "__expr__", + "uid": "__expr__" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel Inheriting from Target", + "description": "Tests panel inheriting datasource from target when panel datasource was default", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with Named Datasource", + "description": "Tests panel with datasource referenced by name - should migrate to full object", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Simple Row Panel", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row with Nested Panels", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-14" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Datasource Reference Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "query_var_null", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "query_var_named", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "query_var_uid", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "query_var_unknown", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "datasource": { + "type": "prometheus", + "uid": "unknown-datasource" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "non_query_var", + "query": "", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2beta1.json new file mode 100644 index 00000000000..6465fae7ed0 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2beta1.json @@ -0,0 +1,1069 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v36.ds_name_to_ref.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "Default Annotation - Tests default datasource migration" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "Named Datasource Annotation - Tests migration by datasource name" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "UID Datasource Annotation - Tests migration by datasource UID" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "Null Datasource Annotation - Tests null datasource fallback to default" + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "unknown-datasource-name" + }, + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "", + "name": "Unknown Datasource Annotation - Tests unknown datasource preserved as UID" + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Null Datasource and Targets", + "description": "Tests null panel datasource migration with targets - should fallback to default", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Panel with UID Datasource", + "description": "Tests panel with datasource referenced by UID - should migrate to full object", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "Nested Panel with Default Datasource", + "description": "Nested panel in collapsed row with default datasource", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-14": { + "kind": "Panel", + "spec": { + "id": 14, + "title": "Nested Panel with Unknown Datasource", + "description": "Nested panel in collapsed row with unknown datasource", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with Null Datasource and Empty Targets", + "description": "Tests null panel datasource with empty targets array - should create default target", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with No Targets Array", + "description": "Tests null panel datasource with missing targets - should create default target array", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with Mixed Datasources", + "description": "Tests mixed datasource panel - targets should migrate independently", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with Existing Object Datasource", + "description": "Tests panel with already migrated datasource object - should preserve existing refs", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with Unknown Datasource Name", + "description": "Tests panel with unknown datasource - should preserve as UID-only reference", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "unknown-target-datasource" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel with Expression Query", + "description": "Tests panel with expression query - should not inherit expression as panel datasource", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "__expr__", + "version": "v0", + "datasource": { + "name": "__expr__" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel Inheriting from Target", + "description": "Tests panel inheriting datasource from target when panel datasource was default", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with Named Datasource", + "description": "Tests panel with datasource referenced by name - should migrate to full object", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Simple Row Panel", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row with Nested Panels", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-14" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Datasource Reference Migration Test Dashboard", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "query_var_null", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "query_var_named", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "query_var_uid", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "query_var_unknown", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "never", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "unknown-datasource" + }, + "spec": {} + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "non_query_var", + "query": "", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "skipUrlSync": false + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v0alpha1.json new file mode 100644 index 00000000000..074c0ef162a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v0alpha1.json @@ -0,0 +1,174 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v37.legend_normalization.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "id": 1, + "options": { + "legend": true + }, + "title": "Panel with Boolean Legend True", + "type": "timeseries" + }, + { + "id": 2, + "options": { + "legend": false + }, + "title": "Panel with Boolean Legend False", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "id": 3, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Panel with Hidden DisplayMode", + "type": "timeseries" + }, + { + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Panel with ShowLegend False", + "type": "stat" + }, + { + "id": 5, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true + } + }, + "title": "Panel with Table Legend", + "type": "barchart" + }, + { + "id": 6, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + } + }, + "title": "Panel with List Legend", + "type": "histogram" + }, + { + "id": 7, + "title": "Panel with No Options", + "type": "text" + }, + { + "id": 8, + "options": { + "reduceOptions": { + "fields": "/.*temperature.*/" + } + }, + "title": "Panel with No Legend Config", + "type": "gauge" + }, + { + "id": 9, + "options": {}, + "title": "Panel with Null Legend", + "type": "piechart" + }, + { + "collapsed": false, + "id": 10, + "panels": [ + { + "id": 11, + "options": { + "legend": true + }, + "title": "Nested Panel with Boolean Legend", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "id": 12, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Nested Panel with Hidden DisplayMode", + "type": "timeseries" + }, + { + "id": 13, + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "title": "Nested Panel with Conflicting Properties", + "type": "stat" + } + ], + "title": "Row with Nested Panels Having Various Legend Configs", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V37 Legend Normalization Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2alpha1.json new file mode 100644 index 00000000000..55f6c000481 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2alpha1.json @@ -0,0 +1,633 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v37.legend_normalization.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Boolean Legend True", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Nested Panel with Boolean Legend", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Nested Panel with Hidden DisplayMode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "Nested Panel with Conflicting Properties", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with Boolean Legend False", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": false + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with Hidden DisplayMode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ShowLegend False", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with Table Legend", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with List Legend", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "histogram", + "spec": { + "pluginVersion": "", + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel with No Options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "text", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with No Legend Config", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": { + "reduceOptions": { + "fields": "/.*temperature.*/" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with Null Legend", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "piechart", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with Nested Panels Having Various Legend Configs", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V37 Legend Normalization Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2beta1.json new file mode 100644 index 00000000000..2803cc68947 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2beta1.json @@ -0,0 +1,646 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v37.legend_normalization.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Boolean Legend True", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Nested Panel with Boolean Legend", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Nested Panel with Hidden DisplayMode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "id": 13, + "title": "Nested Panel with Conflicting Properties", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with Boolean Legend False", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": false + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with Hidden DisplayMode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ShowLegend False", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": { + "legend": { + "displayMode": "list", + "showLegend": false + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with Table Legend", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with List Legend", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "histogram", + "version": "", + "spec": { + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel with No Options", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "text", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with No Legend Config", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": { + "reduceOptions": { + "fields": "/.*temperature.*/" + } + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Panel with Null Legend", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "piechart", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with Nested Panels Having Various Legend Configs", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-13" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V37 Legend Normalization Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v0alpha1.json new file mode 100644 index 00000000000..2bb56b9a8a3 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v0alpha1.json @@ -0,0 +1,268 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v38.table_displaymode_comprehensive.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + }, + "id": 1, + "title": "Table with Basic Gauge", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [] + }, + "id": 2, + "title": "Table with Gradient Gauge", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + } + }, + "overrides": [] + }, + "id": 3, + "title": "Table with LCD Gauge", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + } + }, + "overrides": [] + }, + "id": 4, + "title": "Table with Color Background", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + } + }, + "overrides": [] + }, + "id": 5, + "title": "Table with Color Background Solid", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + } + }, + "overrides": [] + }, + "id": 6, + "title": "Table with Unknown Mode", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "width": 100 + } + }, + "overrides": [] + }, + "id": 7, + "title": "Table with No Display Mode", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + } + ] + } + ] + }, + "id": 8, + "title": "Table with Overrides", + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "id": 9, + "title": "Non-table Panel (Should Remain Unchanged)", + "type": "timeseries" + }, + { + "collapsed": false, + "id": 10, + "panels": [ + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + }, + "id": 11, + "title": "Nested Table with Basic Mode", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NestedField" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + } + ] + } + ] + }, + "id": 12, + "title": "Nested Table with Gradient Gauge", + "type": "table" + } + ], + "title": "Row with Nested Table Panels", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V38 Table Migration Comprehensive Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2alpha1.json new file mode 100644 index 00000000000..66bb53a1033 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2alpha1.json @@ -0,0 +1,663 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v38.table_displaymode_comprehensive.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Table with Basic Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Nested Table with Basic Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Nested Table with Gradient Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NestedField" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Table with Gradient Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Table with LCD Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Table with Color Background", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Table with Color Background Solid", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Table with Unknown Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Table with No Display Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "width": 100 + } + }, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Table with Overrides", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + } + ] + } + ] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Non-table Panel (Should Remain Unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with Nested Table Panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V38 Table Migration Comprehensive Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2beta1.json new file mode 100644 index 00000000000..493b6147404 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2beta1.json @@ -0,0 +1,675 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v38.table_displaymode_comprehensive.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Table with Basic Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Nested Table with Basic Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Nested Table with Gradient Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NestedField" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Table with Gradient Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Table with LCD Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Table with Color Background", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Table with Color Background Solid", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Table with Unknown Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Table with No Display Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "width": 100 + } + }, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Table with Overrides", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + } + ] + } + ] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Non-table Panel (Should Remain Unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with Nested Table Panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V38 Table Migration Comprehensive Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v0alpha1.json new file mode 100644 index 00000000000..98e6f272216 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v0alpha1.json @@ -0,0 +1,268 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v38.timeseries_table_display_mode.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + }, + "id": 1, + "title": "Table with Basic Gauge", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [] + }, + "id": 2, + "title": "Table with Gradient Gauge", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + } + }, + "overrides": [] + }, + "id": 3, + "title": "Table with LCD Gauge", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + } + }, + "overrides": [] + }, + "id": 4, + "title": "Table with Color Background", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + } + }, + "overrides": [] + }, + "id": 5, + "title": "Table with Color Background Solid", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + } + }, + "overrides": [] + }, + "id": 6, + "title": "Table with Unknown Mode", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "width": 100 + } + }, + "overrides": [] + }, + "id": 7, + "title": "Table with No Display Mode", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + } + ] + } + ] + }, + "id": 8, + "title": "Table with Overrides", + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "id": 9, + "title": "Non-table Panel (Should Remain Unchanged)", + "type": "timeseries" + }, + { + "collapsed": false, + "id": 10, + "panels": [ + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + }, + "id": 11, + "title": "Nested Table with Basic Mode", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NestedField" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + } + ] + } + ] + }, + "id": 12, + "title": "Nested Table with Gradient Gauge", + "type": "table" + } + ], + "title": "Row with Nested Table Panels", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V38 Table Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2alpha1.json new file mode 100644 index 00000000000..52b313e6b4b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2alpha1.json @@ -0,0 +1,663 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v38.timeseries_table_display_mode.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Table with Basic Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Nested Table with Basic Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Nested Table with Gradient Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NestedField" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Table with Gradient Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Table with LCD Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Table with Color Background", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Table with Color Background Solid", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Table with Unknown Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Table with No Display Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "width": 100 + } + }, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Table with Overrides", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + } + ] + } + ] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Non-table Panel (Should Remain Unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with Nested Table Panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V38 Table Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2beta1.json new file mode 100644 index 00000000000..81cc875df07 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2beta1.json @@ -0,0 +1,675 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v38.timeseries_table_display_mode.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Table with Basic Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "id": 11, + "title": "Nested Table with Basic Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "id": 12, + "title": "Nested Table with Gradient Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NestedField" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Table with Gradient Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Table with LCD Gauge", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "lcd", + "type": "gauge" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Table with Color Background", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "gradient", + "type": "color-background" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Table with Color Background Solid", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "color-background" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Table with Unknown Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "type": "some-other-mode" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Table with No Display Mode", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "width": 100 + } + }, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Table with Overrides", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "mode": "basic", + "type": "gauge" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + } + ] + } + ] + } + } + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "id": 9, + "title": "Non-table Panel (Should Remain Unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-9" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with Nested Table Panels", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-11" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-12" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V38 Table Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v0alpha1.json new file mode 100644 index 00000000000..c847c72890b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v0alpha1.json @@ -0,0 +1,205 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v39.transform_timeseries_table.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "id": 1, + "title": "Panel with TimeSeriesTable Transformation - Single Stat", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "mean" + } + } + } + ], + "type": "table" + }, + { + "id": 2, + "title": "Panel with TimeSeriesTable Transformation - Multiple Stats", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "mean" + }, + "B": { + "stat": "max" + }, + "C": { + "stat": "min" + }, + "D": { + "stat": "sum" + } + } + } + ], + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "id": 3, + "title": "Panel with TimeSeriesTable Transformation - Mixed with Other Transforms", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": [ + "mean" + ] + } + }, + { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "last" + }, + "B": { + "stat": "first" + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {} + } + } + ], + "type": "timeseries" + }, + { + "id": 4, + "title": "Panel with Non-TimeSeriesTable Transformation (Should Remain Unchanged)", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": [ + "mean", + "max" + ] + } + } + ], + "type": "stat" + }, + { + "id": 5, + "title": "Panel with TimeSeriesTable - Empty RefIdToStat", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + } + ], + "type": "table" + }, + { + "id": 6, + "title": "Panel with TimeSeriesTable - No Options (Should Skip)", + "transformations": [ + { + "id": "timeSeriesTable" + } + ], + "type": "table" + }, + { + "id": 7, + "title": "Panel with TimeSeriesTable - Invalid Options (Should Skip)", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "someOtherOption": "value" + } + } + ], + "type": "table" + }, + { + "autoMigrateFrom": "graph", + "id": 8, + "title": "Panel with No Transformations (Should Remain Unchanged)", + "type": "timeseries" + }, + { + "collapsed": false, + "id": 9, + "panels": [ + { + "id": 10, + "title": "Nested Panel with TimeSeriesTable", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "NestedA": { + "stat": "median" + }, + "NestedB": { + "stat": "stdDev" + } + } + } + ], + "type": "table" + } + ], + "title": "Row with Nested Panels Having TimeSeriesTable Transformations", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V39 TimeSeriesTable Transformation Migration Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2alpha1.json new file mode 100644 index 00000000000..df196e14beb --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2alpha1.json @@ -0,0 +1,590 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v39.transform_timeseries_table.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with TimeSeriesTable Transformation - Single Stat", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "mean" + } + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Nested Panel with TimeSeriesTable", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "NestedA": { + "stat": "median" + }, + "NestedB": { + "stat": "stdDev" + } + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with TimeSeriesTable Transformation - Multiple Stats", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "mean" + }, + "B": { + "stat": "max" + }, + "C": { + "stat": "min" + }, + "D": { + "stat": "sum" + } + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with TimeSeriesTable Transformation - Mixed with Other Transforms", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "reducers": [ + "mean" + ] + } + } + }, + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "last" + }, + "B": { + "stat": "first" + } + } + } + }, + { + "kind": "organize", + "spec": { + "id": "organize", + "options": { + "excludeByName": {} + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with Non-TimeSeriesTable Transformation (Should Remain Unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "reducers": [ + "mean", + "max" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with TimeSeriesTable - Empty RefIdToStat", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with TimeSeriesTable - No Options (Should Skip)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": null + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel with TimeSeriesTable - Invalid Options (Should Skip)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "someOtherOption": "value" + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with No Transformations (Should Remain Unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with Nested Panels Having TimeSeriesTable Transformations", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V39 TimeSeriesTable Transformation Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2beta1.json new file mode 100644 index 00000000000..3b56960e026 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2beta1.json @@ -0,0 +1,600 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v39.transform_timeseries_table.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with TimeSeriesTable Transformation - Single Stat", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "mean" + } + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "id": 10, + "title": "Nested Panel with TimeSeriesTable", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "NestedA": { + "stat": "median" + }, + "NestedB": { + "stat": "stdDev" + } + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with TimeSeriesTable Transformation - Multiple Stats", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "mean" + }, + "B": { + "stat": "max" + }, + "C": { + "stat": "min" + }, + "D": { + "stat": "sum" + } + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with TimeSeriesTable Transformation - Mixed with Other Transforms", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "reducers": [ + "mean" + ] + } + } + }, + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "A": { + "stat": "last" + }, + "B": { + "stat": "first" + } + } + } + }, + { + "kind": "organize", + "spec": { + "id": "organize", + "options": { + "excludeByName": {} + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with Non-TimeSeriesTable Transformation (Should Remain Unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "reducers": [ + "mean", + "max" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel with TimeSeriesTable - Empty RefIdToStat", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": {} + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with TimeSeriesTable - No Options (Should Skip)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": null + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel with TimeSeriesTable - Invalid Options (Should Skip)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [ + { + "kind": "timeSeriesTable", + "spec": { + "id": "timeSeriesTable", + "options": { + "someOtherOption": "value" + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "id": 8, + "title": "Panel with No Transformations (Should Remain Unchanged)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-8" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with Nested Panels Having TimeSeriesTable Transformations", + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-10" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V39 TimeSeriesTable Transformation Migration Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v0alpha1.json new file mode 100644 index 00000000000..a41d82c8feb --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v0alpha1.json @@ -0,0 +1,111 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v4.no-op.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "stat" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "stat" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V4 No-Op Migration Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2alpha1.json new file mode 100644 index 00000000000..1a3bd5c0e72 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2alpha1.json @@ -0,0 +1,241 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v4.no-op.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V4 No-Op Migration Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2beta1.json new file mode 100644 index 00000000000..b6d1aa7d200 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2beta1.json @@ -0,0 +1,248 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v4.no-op.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V4 No-Op Migration Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v0alpha1.json new file mode 100644 index 00000000000..a68f9fdd2e4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v0alpha1.json @@ -0,0 +1,50 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v40.refresh_empty_string.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Empty String Refresh Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2alpha1.json new file mode 100644 index 00000000000..db8e3a3542a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v40.refresh_empty_string.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Empty String Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2beta1.json new file mode 100644 index 00000000000..a6cabe62d43 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v40.refresh_empty_string.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Empty String Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v0alpha1.json new file mode 100644 index 00000000000..ee3991c8f2a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v0alpha1.json @@ -0,0 +1,50 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v40.refresh_false.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Boolean False Refresh Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2alpha1.json new file mode 100644 index 00000000000..bd834847e28 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v40.refresh_false.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Boolean False Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2beta1.json new file mode 100644 index 00000000000..7b5f900ef36 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v40.refresh_false.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Boolean False Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v0alpha1.json new file mode 100644 index 00000000000..a6063fed38e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v0alpha1.json @@ -0,0 +1,50 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v40.refresh_not_set.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Refresh Not Set Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2alpha1.json new file mode 100644 index 00000000000..8279dfd765b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v40.refresh_not_set.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Refresh Not Set Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2beta1.json new file mode 100644 index 00000000000..1f09949f995 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v40.refresh_not_set.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Refresh Not Set Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v0alpha1.json new file mode 100644 index 00000000000..fbd4b0e1486 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v0alpha1.json @@ -0,0 +1,50 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v40.refresh_numeric.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Numeric Refresh Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2alpha1.json new file mode 100644 index 00000000000..2cc922fc5f5 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v40.refresh_numeric.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Numeric Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2beta1.json new file mode 100644 index 00000000000..6904267e72b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v40.refresh_numeric.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Numeric Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v0alpha1.json new file mode 100644 index 00000000000..d1f0fe07cd7 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v0alpha1.json @@ -0,0 +1,50 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v40.refresh_string.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "1m", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "String Refresh Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2alpha1.json new file mode 100644 index 00000000000..d9347950655 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v40.refresh_string.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "1m", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "String Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2beta1.json new file mode 100644 index 00000000000..b1bd81269c1 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v40.refresh_string.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "1m", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "String Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v0alpha1.json new file mode 100644 index 00000000000..e7103268a45 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v0alpha1.json @@ -0,0 +1,50 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v40.refresh_true.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Boolean Refresh Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2alpha1.json new file mode 100644 index 00000000000..9601af1797a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v40.refresh_true.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Boolean Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2beta1.json new file mode 100644 index 00000000000..76eea7dbbdc --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v40.refresh_true.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Boolean Refresh Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v0alpha1.json new file mode 100644 index 00000000000..0dbd900c8ef --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v0alpha1.json @@ -0,0 +1,50 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v41.no_time_picker.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "No Time Picker Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2alpha1.json new file mode 100644 index 00000000000..6f34667e603 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v41.no_time_picker.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "No Time Picker Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2beta1.json new file mode 100644 index 00000000000..54e2cfeef43 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v41.no_time_picker.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "No Time Picker Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v0alpha1.json new file mode 100644 index 00000000000..52099b239f3 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v0alpha1.json @@ -0,0 +1,62 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v41.time_picker_no_time_options.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Time Picker No Time Options Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2alpha1.json new file mode 100644 index 00000000000..46a07c16b0e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v41.time_picker_no_time_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Time Picker No Time Options Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2beta1.json new file mode 100644 index 00000000000..52de1909de4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v41.time_picker_no_time_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Time Picker No Time Options Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v0alpha1.json new file mode 100644 index 00000000000..739677d77f5 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v0alpha1.json @@ -0,0 +1,63 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v41.time_picker_time_options.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Time Picker Time Options Test Dashboard", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2alpha1.json new file mode 100644 index 00000000000..15368de002c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2alpha1.json @@ -0,0 +1,68 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v41.time_picker_time_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Time Picker Time Options Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2beta1.json new file mode 100644 index 00000000000..9dffb1fc27e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2beta1.json @@ -0,0 +1,69 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v41.time_picker_time_options.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Time Picker Time Options Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v0alpha1.json new file mode 100644 index 00000000000..94d5e062fd1 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v0alpha1.json @@ -0,0 +1,75 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v42.harky_must.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "id": 1, + "title": "Panel with hideFrom.viz = true", + "type": "timeseries" + } + ], + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "v42 Migration Test - Harky Must", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2alpha1.json new file mode 100644 index 00000000000..d2e8c73301e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2alpha1.json @@ -0,0 +1,127 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v42.harky_must.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with hideFrom.viz = true", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "v42 Migration Test - Harky Must", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2beta1.json new file mode 100644 index 00000000000..3d267c3135b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2beta1.json @@ -0,0 +1,129 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v42.harky_must.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with hideFrom.viz = true", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "v42 Migration Test - Harky Must", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v0alpha1.json new file mode 100644 index 00000000000..eb821d0672c --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v0alpha1.json @@ -0,0 +1,233 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v42.hidefrom_tooltip.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "id": 1, + "title": "Panel with hideFrom.viz = true", + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "foo" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "id": 2, + "title": "Panel with multiple overrides", + "type": "timeseries" + }, + { + "collapsed": true, + "id": 3, + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*/" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "id": 4, + "title": "Nested panel with hideFrom", + "type": "stat" + }, + { + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "unit", + "value": "short" + } + ] + } + ] + }, + "id": 5, + "title": "Panel without hideFrom", + "type": "table" + } + ], + "title": "Row with nested panels", + "type": "row" + }, + { + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsZero", + "value": 0 + } + }, + "properties": [ + { + "id": "unit", + "value": "short" + } + ] + } + ] + }, + "id": 6, + "title": "Panel with viz false (should not be modified)", + "type": "gauge" + }, + { + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "{__name__=\"ALERTS\", alertname=\"k6CloudServiceErrorsLogged\"}" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "id": 7, + "title": "Panel with already set tooltip (should not be modified)", + "type": "barchart" + } + ], + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "v42 Migration Test - HideFrom Tooltip", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2alpha1.json new file mode 100644 index 00000000000..9ef09aa0a9b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2alpha1.json @@ -0,0 +1,465 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v42.hidefrom_tooltip.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with hideFrom.viz = true", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with multiple overrides", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "foo" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Nested panel with hideFrom", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*/" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel without hideFrom", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "unit", + "value": "short" + } + ] + } + ] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with viz false (should not be modified)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "gauge", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsZero", + "value": 0 + } + }, + "properties": [ + { + "id": "unit", + "value": "short" + } + ] + } + ] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel with already set tooltip (should not be modified)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "{__name__=\"ALERTS\", alertname=\"k6CloudServiceErrorsLogged\"}" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": true, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with nested panels", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "v42 Migration Test - HideFrom Tooltip", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2beta1.json new file mode 100644 index 00000000000..a1e187676a5 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2beta1.json @@ -0,0 +1,472 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v42.hidefrom_tooltip.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with hideFrom.viz = true", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Panel with multiple overrides", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field2" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "foo" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Nested panel with hideFrom", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*/" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Panel without hideFrom", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "unit", + "value": "short" + } + ] + } + ] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Panel with viz false (should not be modified)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "gauge", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsZero", + "value": 0 + } + }, + "properties": [ + { + "id": "unit", + "value": "short" + } + ] + } + ] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Panel with already set tooltip (should not be modified)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "{__name__=\"ALERTS\", alertname=\"k6CloudServiceErrorsLogged\"}" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": true, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row with nested panels", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "v42 Migration Test - HideFrom Tooltip", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v0alpha1.json new file mode 100644 index 00000000000..706040137db --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v0alpha1.json @@ -0,0 +1,111 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v5.no-op.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "stat" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "stat" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V5 No-Op Migration Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2alpha1.json new file mode 100644 index 00000000000..5855379dc98 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2alpha1.json @@ -0,0 +1,241 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v5.no-op.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V5 No-Op Migration Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2beta1.json new file mode 100644 index 00000000000..10544c3c0fe --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2beta1.json @@ -0,0 +1,248 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v5.no-op.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V5 No-Op Migration Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v0alpha1.json new file mode 100644 index 00000000000..75a9fa83f62 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v0alpha1.json @@ -0,0 +1,171 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v6.pulldowns_and_templating.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + }, + { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "enable": true, + "iconColor": "red", + "name": "deployment", + "query": "ALERTS{alertname=\"DeploymentStarted\"}" + }, + { + "datasource": { + "type": "loki", + "uid": "loki-uid" + }, + "enable": false, + "iconColor": "yellow", + "name": "alerts", + "query": "{job=\"alertmanager\"}" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "cpu_usage{environment=\"$environment\", service=\"$service\"}", + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "memory_usage{region=\"$region\"}", + "refId": "B" + } + ], + "title": "Memory Usage", + "type": "stat" + } + ], + "refresh": "30s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "name": "environment", + "options": [], + "query": "label_values(up, instance)", + "refresh": 1, + "type": "query" + }, + { + "allFormat": "glob", + "datasource": "prometheus", + "name": "service", + "options": [], + "query": "label_values(up, instance)", + "refresh": 1, + "type": "query" + }, + { + "name": "region", + "options": [ + { + "text": "us-east-1", + "value": "us-east-1" + }, + { + "text": "us-west-2", + "value": "us-west-2" + } + ], + "type": "custom" + }, + { + "datasource": "prometheus", + "name": "instance", + "options": [], + "query": "label_values(up, instance)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V6 Pulldowns and Template Variables Migration Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2alpha1.json new file mode 100644 index 00000000000..de1bac8f2f5 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2alpha1.json @@ -0,0 +1,336 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v6.pulldowns_and_templating.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "deployment", + "legacyOptions": { + "query": "ALERTS{alertname=\"DeploymentStarted\"}" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "loki", + "uid": "loki-uid" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "yellow", + "name": "alerts", + "legacyOptions": { + "query": "{job=\"alertmanager\"}" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "cpu_usage{environment=\"$environment\", service=\"$service\"}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "memory_usage{region=\"$region\"}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-1h", + "to": "now", + "autoRefresh": "30s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V6 Pulldowns and Template Variables Migration Test", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "environment", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "service", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "region", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "us-east-1", + "value": "us-east-1" + }, + { + "selected": false, + "text": "us-west-2", + "value": "us-west-2" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "instance", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2beta1.json new file mode 100644 index 00000000000..3562d0154e1 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2beta1.json @@ -0,0 +1,348 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v6.pulldowns_and_templating.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "deployment", + "legacyOptions": { + "query": "ALERTS{alertname=\"DeploymentStarted\"}" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "loki-uid" + }, + "spec": {} + }, + "enable": false, + "hide": false, + "iconColor": "yellow", + "name": "alerts", + "legacyOptions": { + "query": "{job=\"alertmanager\"}" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "CPU Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "cpu_usage{environment=\"$environment\", service=\"$service\"}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Memory Usage", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "memory_usage{region=\"$region\"}" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-1h", + "to": "now", + "autoRefresh": "30s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V6 Pulldowns and Template Variables Migration Test", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "environment", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "service", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "region", + "query": "", + "current": { + "text": "", + "value": "" + }, + "options": [ + { + "selected": false, + "text": "us-east-1", + "value": "us-east-1" + }, + { + "selected": false, + "text": "us-west-2", + "value": "us-west-2" + } + ], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "instance", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v0alpha1.json new file mode 100644 index 00000000000..4bb069c11a3 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v0alpha1.json @@ -0,0 +1,98 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v7.timepicker.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "up", + "refId": "A" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "expr": "cpu_usage", + "refId": "B" + } + ] + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "type": "timepicker" + }, + "timezone": "", + "title": "No Title", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2alpha1.json new file mode 100644 index 00000000000..48cdb34c46b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2alpha1.json @@ -0,0 +1,146 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v7.timepicker.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "up" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "cpu_usage" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "No Title", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2beta1.json new file mode 100644 index 00000000000..72cb9aadb9b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2beta1.json @@ -0,0 +1,150 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v7.timepicker.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "up" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "cpu_usage" + } + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "No Title", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v0alpha1.json new file mode 100644 index 00000000000..95b06ba7ef4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v0alpha1.json @@ -0,0 +1,156 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v8.influxdb_query.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "groupBy": [ + { + "params": [ + "1m" + ], + "type": "time" + }, + { + "params": [ + "host" + ], + "type": "tag" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "refId": "A", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + }, + { + "params": [ + "*2" + ], + "type": "math" + }, + { + "params": [ + "doubled" + ], + "type": "alias" + } + ] + ], + "tags": [ + { + "key": "host", + "value": "server1" + } + ] + } + ], + "type": "timeseries" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "groupBy": [ + { + "interval": "1m", + "type": "time" + } + ], + "rawQuery": true, + "refId": "A", + "tags": [ + { + "key": "host", + "value": "server1" + } + ] + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V8 InfluxDB Query Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2alpha1.json new file mode 100644 index 00000000000..75a2970f8b6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2alpha1.json @@ -0,0 +1,251 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v8.influxdb_query.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "groupBy": [ + { + "params": [ + "1m" + ], + "type": "time" + }, + { + "params": [ + "host" + ], + "type": "tag" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + }, + { + "params": [ + "*2" + ], + "type": "math" + }, + { + "params": [ + "doubled" + ], + "type": "alias" + } + ] + ], + "tags": [ + { + "key": "host", + "value": "server1" + } + ] + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "groupBy": [ + { + "interval": "1m", + "type": "time" + } + ], + "rawQuery": true, + "tags": [ + { + "key": "host", + "value": "server1" + } + ] + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V8 InfluxDB Query Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2beta1.json new file mode 100644 index 00000000000..d8ee1076605 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2beta1.json @@ -0,0 +1,256 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v8.influxdb_query.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "groupBy": [ + { + "params": [ + "1m" + ], + "type": "time" + }, + { + "params": [ + "host" + ], + "type": "tag" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + }, + { + "params": [ + "*2" + ], + "type": "math" + }, + { + "params": [ + "doubled" + ], + "type": "alias" + } + ] + ], + "tags": [ + { + "key": "host", + "value": "server1" + } + ] + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "groupBy": [ + { + "interval": "1m", + "type": "time" + } + ], + "rawQuery": true, + "tags": [ + { + "key": "host", + "value": "server1" + } + ] + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V8 InfluxDB Query Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v0alpha1.json new file mode 100644 index 00000000000..4c7255237ba --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v0alpha1.json @@ -0,0 +1,111 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "v9.no-op.v42" + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "stat" + }, + { + "autoMigrateFrom": "singlestat", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "stat" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "V9 No-Op Migration Test", + "weekStart": "" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2alpha1.json new file mode 100644 index 00000000000..472d28ffa99 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2alpha1.json @@ -0,0 +1,241 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "v9.no-op.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V9 No-Op Migration Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2beta1.json new file mode 100644 index 00000000000..45487aaebb9 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2beta1.json @@ -0,0 +1,248 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "v9.no-op.v42" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "V9 No-Op Migration Test", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json new file mode 100644 index 00000000000..1f0c641c7a6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v0alpha1.json @@ -0,0 +1,230 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "annotation-conversions-test", + "labels": { + "test": "annotation-conversions" + } + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "builtIn": 0, + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "Prometheus Annotations", + "target": { + "expr": "ALERTS{alertstate=\"firing\"}", + "interval": "1m", + "refId": "Anno", + "step": 60 + }, + "type": "prometheus" + }, + { + "builtIn": false, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "TestData Annotations", + "target": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + }, + "type": "testdata" + }, + { + "builtIn": 0, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Elasticsearch Annotations", + "tagsField": "tags", + "target": { + "query": "tags:deployment", + "refId": "Anno", + "timeField": "@timestamp" + }, + "textField": "message", + "timeEndField": "end_time", + "timeField": "@timestamp", + "type": "elasticsearch" + }, + { + "builtIn": 0, + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "enable": true, + "hide": false, + "iconColor": "green", + "name": "SQL Annotations", + "tagsColumn": "tags", + "target": { + "format": "table", + "rawSql": "SELECT time, title, text, tags FROM annotations WHERE $__timeFilter(time)", + "refId": "Anno" + }, + "textColumn": "text", + "timeColumn": "time", + "titleColumn": "title", + "type": "sql" + }, + { + "builtIn": 0, + "enable": true, + "hide": false, + "iconColor": "purple", + "name": "No Datasource Annotation", + "target": { + "expr": "static_annotation_query", + "refId": "Anno" + }, + "type": "manual" + }, + { + "builtIn": 0, + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "enable": false, + "hide": true, + "iconColor": "gray", + "name": "Hidden Annotation", + "target": { + "expr": "disabled_query", + "refId": "Anno" + }, + "type": "prometheus" + }, + { + "builtIn": 0, + "datasource": { + "type": "influxdb", + "uid": "influx-uid" + }, + "enable": true, + "filter": { + "exclude": true, + "ids": [ + 1, + 2, + 3 + ] + }, + "hide": false, + "iconColor": "#FF5722", + "mappings": { + "tags": "labels", + "text": "description", + "time": "timestamp", + "title": "service" + }, + "name": "Complex Filter Annotation", + "target": { + "query": "from(bucket: \"events\") |\u003e range(start: $__timeFrom, stop: $__timeTo) |\u003e filter(fn: (r) =\u003e r._measurement == \"deployments\")", + "refId": "Anno" + }, + "type": "influxdb" + }, + { + "builtIn": 0, + "datasource": { + "type": "cloudwatch", + "uid": "cloudwatch-uid" + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotations", + "showIn": 0, + "target": { + "dimensions": { + "InstanceId": "i-1234567890abcdef" + }, + "metricName": "CPUUtilization", + "namespace": "AWS/EC2", + "refId": "Anno", + "statistic": "Average" + }, + "type": "cloudwatch" + }, + { + "builtIn": true, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "enable": false, + "hide": true, + "iconColor": "transparent", + "name": "Disabled Built-in Annotation", + "target": { + "expr": "ALERTS{alertstate=\"firing\"}", + "interval": "1m", + "refId": "Anno", + "step": 60 + }, + "type": "prometheus" + } + ] + }, + "description": "Testing annotation transformations", + "links": [], + "panels": [], + "tags": [ + "test", + "annotations" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "title": "Annotation Conversions Test Dashboard" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json new file mode 100644 index 00000000000..9324355a72f --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json @@ -0,0 +1,327 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "annotation-conversions-test", + "labels": { + "test": "annotation-conversions" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "DataQuery", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "expr": "ALERTS{alertstate=\"firing\"}", + "interval": "1m", + "refId": "Anno", + "step": 60 + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "Prometheus Annotations", + "builtIn": false, + "legacyOptions": { + "type": "prometheus" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "TestData Annotations", + "builtIn": false, + "legacyOptions": { + "type": "testdata" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "query": "tags:deployment", + "refId": "Anno", + "timeField": "@timestamp" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Elasticsearch Annotations", + "builtIn": false, + "legacyOptions": { + "tagsField": "tags", + "textField": "message", + "timeEndField": "end_time", + "timeField": "@timestamp", + "type": "elasticsearch" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "format": "table", + "rawSql": "SELECT time, title, text, tags FROM annotations WHERE $__timeFilter(time)", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "green", + "name": "SQL Annotations", + "builtIn": false, + "legacyOptions": { + "tagsColumn": "tags", + "textColumn": "text", + "timeColumn": "time", + "titleColumn": "title", + "type": "sql" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "static_annotation_query", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "purple", + "name": "No Datasource Annotation", + "builtIn": false, + "legacyOptions": { + "type": "manual" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "expr": "disabled_query", + "refId": "Anno" + } + }, + "enable": false, + "hide": true, + "iconColor": "gray", + "name": "Hidden Annotation", + "builtIn": false, + "legacyOptions": { + "type": "prometheus" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "influxdb", + "uid": "influx-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "query": "from(bucket: \"events\") |\u003e range(start: $__timeFrom, stop: $__timeTo) |\u003e filter(fn: (r) =\u003e r._measurement == \"deployments\")", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "#FF5722", + "name": "Complex Filter Annotation", + "builtIn": false, + "filter": { + "exclude": true, + "ids": [ + 1, + 2, + 3 + ] + }, + "legacyOptions": { + "mappings": { + "tags": "labels", + "text": "description", + "time": "timestamp", + "title": "service" + }, + "type": "influxdb" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "cloudwatch", + "uid": "cloudwatch-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "dimensions": { + "InstanceId": "i-1234567890abcdef" + }, + "metricName": "CPUUtilization", + "namespace": "AWS/EC2", + "refId": "Anno", + "statistic": "Average" + } + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotations", + "builtIn": false, + "legacyOptions": { + "showIn": 0, + "type": "cloudwatch" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "expr": "ALERTS{alertstate=\"firing\"}", + "interval": "1m", + "refId": "Anno", + "step": 60 + } + }, + "enable": false, + "hide": true, + "iconColor": "transparent", + "name": "Disabled Built-in Annotation", + "builtIn": true, + "legacyOptions": { + "type": "prometheus" + } + } + } + ], + "cursorSync": "Off", + "description": "Testing annotation transformations", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "annotations" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-24h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Annotation Conversions Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2beta1.json new file mode 100644 index 00000000000..3f0eb4d3eb4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2beta1.json @@ -0,0 +1,338 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "annotation-conversions-test", + "labels": { + "test": "annotation-conversions" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true, + "legacyOptions": { + "type": "dashboard" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "existing-ref-uid" + }, + "spec": { + "expr": "ALERTS{alertstate=\"firing\"}", + "interval": "1m", + "refId": "Anno", + "step": 60 + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "Prometheus Annotations", + "builtIn": false, + "legacyOptions": { + "type": "prometheus" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "TestData Annotations", + "builtIn": false, + "legacyOptions": { + "type": "testdata" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": { + "query": "tags:deployment", + "refId": "Anno", + "timeField": "@timestamp" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Elasticsearch Annotations", + "builtIn": false, + "legacyOptions": { + "tagsField": "tags", + "textField": "message", + "timeEndField": "end_time", + "timeField": "@timestamp", + "type": "elasticsearch" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "non-default-test-ds-uid" + }, + "spec": { + "format": "table", + "rawSql": "SELECT time, title, text, tags FROM annotations WHERE $__timeFilter(time)", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "green", + "name": "SQL Annotations", + "builtIn": false, + "legacyOptions": { + "tagsColumn": "tags", + "textColumn": "text", + "timeColumn": "time", + "titleColumn": "title", + "type": "sql" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "expr": "static_annotation_query", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "purple", + "name": "No Datasource Annotation", + "builtIn": false, + "legacyOptions": { + "type": "manual" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "existing-ref-uid" + }, + "spec": { + "expr": "disabled_query", + "refId": "Anno" + } + }, + "enable": false, + "hide": true, + "iconColor": "gray", + "name": "Hidden Annotation", + "builtIn": false, + "legacyOptions": { + "type": "prometheus" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "influxdb", + "version": "v0", + "datasource": { + "name": "influx-uid" + }, + "spec": { + "query": "from(bucket: \"events\") |\u003e range(start: $__timeFrom, stop: $__timeTo) |\u003e filter(fn: (r) =\u003e r._measurement == \"deployments\")", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "#FF5722", + "name": "Complex Filter Annotation", + "builtIn": false, + "filter": { + "exclude": true, + "ids": [ + 1, + 2, + 3 + ] + }, + "legacyOptions": { + "mappings": { + "tags": "labels", + "text": "description", + "time": "timestamp", + "title": "service" + }, + "type": "influxdb" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "cloudwatch", + "version": "v0", + "datasource": { + "name": "cloudwatch-uid" + }, + "spec": { + "dimensions": { + "InstanceId": "i-1234567890abcdef" + }, + "metricName": "CPUUtilization", + "namespace": "AWS/EC2", + "refId": "Anno", + "statistic": "Average" + } + }, + "enable": true, + "hide": false, + "iconColor": "orange", + "name": "CloudWatch Annotations", + "builtIn": false, + "legacyOptions": { + "showIn": 0, + "type": "cloudwatch" + } + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "expr": "ALERTS{alertstate=\"firing\"}", + "interval": "1m", + "refId": "Anno", + "step": 60 + } + }, + "enable": false, + "hide": true, + "iconColor": "transparent", + "name": "Disabled Built-in Annotation", + "builtIn": true, + "legacyOptions": { + "type": "prometheus" + } + } + } + ], + "cursorSync": "Off", + "description": "Testing annotation transformations", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "annotations" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-24h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Annotation Conversions Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v0alpha1.json new file mode 100644 index 00000000000..75dd241b415 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v0alpha1.json @@ -0,0 +1,210 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "dashboard-properties-test", + "labels": { + "test": "dashboard-properties" + } + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 0, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "Simple Annotation", + "target": { + "refId": "Anno" + } + } + ] + }, + "description": "Testing dashboard-level property transformations including time settings, cursor sync, and links", + "editable": true, + "fiscalYearStartMonth": 4, + "gnetId": 12345, + "graphTooltip": 2, + "iteration": 1640995200000, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": [ + "monitoring", + "alerts" + ], + "targetBlank": true, + "title": "External Monitoring System", + "tooltip": "View in external system", + "type": "link", + "url": "https://monitoring.example.com/dashboard?from=${__from}\u0026to=${__to}" + }, + { + "asDropdown": true, + "icon": "dashboard", + "includeVars": false, + "keepTime": false, + "tags": [ + "grafana", + "internal" + ], + "targetBlank": false, + "title": "Related Dashboards", + "tooltip": "Navigate to related dashboards", + "type": "dashboards", + "url": "" + }, + { + "asDropdown": false, + "icon": "info", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "Documentation", + "tooltip": "View documentation", + "type": "link", + "url": "https://docs.example.com/dashboard-guide" + }, + { + "asDropdown": false, + "icon": "tag", + "includeVars": true, + "keepTime": true, + "tags": [ + "alerts" + ], + "targetBlank": false, + "title": "Tag-based Link", + "tooltip": "Filtered by tags", + "type": "tag", + "url": "/dashboards/tag/alerts" + } + ], + "liveNow": true, + "meta": { + "canAdmin": false, + "canEdit": true, + "canSave": true, + "canStar": true, + "created": "2023-01-01T00:00:00Z", + "createdBy": "admin", + "expires": "2024-01-01T00:00:00Z", + "folderId": 0, + "folderTitle": "General", + "folderUrl": "", + "hasAcl": false, + "isFolder": false, + "provisioned": false, + "provisionedExternalId": "", + "slug": "dashboard-properties-test", + "updated": "2023-06-01T00:00:00Z", + "updatedBy": "admin", + "url": "/d/test/dashboard-properties-test", + "version": 42 + }, + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": {}, + "targets": [ + { + "refId": "A" + } + ], + "title": "Simple Panel", + "type": "stat" + } + ], + "preload": true, + "refresh": "30s", + "schemaVersion": 39, + "snapshot": { + "originalUrl": "http://localhost:3000/d/test", + "timestamp": "2023-01-01T00:00:00Z" + }, + "style": "dark", + "tags": [ + "test", + "properties", + "metadata" + ], + "templating": { + "list": [ + { + "current": { + "text": "value1", + "value": "value1" + }, + "hide": 0, + "name": "simple_var", + "query": "value1,value2,value3", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-12h", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "hidden": false, + "notice": false, + "now": true, + "nowDelay": "1m", + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + }, + "timezone": "America/New_York", + "title": "Dashboard Properties Test", + "version": 42, + "weekStart": "sunday" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2alpha1.json new file mode 100644 index 00000000000..40810adb95b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2alpha1.json @@ -0,0 +1,203 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "dashboard-properties-test", + "labels": { + "test": "dashboard-properties" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "Simple Annotation", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "Testing dashboard-level property transformations including time settings, cursor sync, and links", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [ + { + "title": "External Monitoring System", + "type": "link", + "icon": "external link", + "tooltip": "View in external system", + "url": "https://monitoring.example.com/dashboard?from=${__from}\u0026to=${__to}", + "tags": [ + "monitoring", + "alerts" + ], + "asDropdown": false, + "targetBlank": true, + "includeVars": true, + "keepTime": true + }, + { + "title": "Related Dashboards", + "type": "dashboards", + "icon": "dashboard", + "tooltip": "Navigate to related dashboards", + "url": "", + "tags": [ + "grafana", + "internal" + ], + "asDropdown": true, + "targetBlank": false, + "includeVars": false, + "keepTime": false + }, + { + "title": "Documentation", + "type": "link", + "icon": "info", + "tooltip": "View documentation", + "url": "https://docs.example.com/dashboard-guide", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Tag-based Link", + "type": "tag", + "icon": "tag", + "tooltip": "Filtered by tags", + "url": "/dashboards/tag/alerts", + "tags": [ + "alerts" + ], + "asDropdown": false, + "targetBlank": false, + "includeVars": true, + "keepTime": true + } + ], + "liveNow": true, + "preload": true, + "tags": [ + "test", + "properties", + "metadata" + ], + "timeSettings": { + "timezone": "America/New_York", + "from": "now-12h", + "to": "now", + "autoRefresh": "30s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "sunday", + "fiscalYearStartMonth": 4, + "nowDelay": "1m" + }, + "title": "Dashboard Properties Test", + "variables": [ + { + "kind": "CustomVariable", + "spec": { + "name": "simple_var", + "query": "value1,value2,value3", + "current": { + "text": "value1", + "value": "value1" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2beta1.json new file mode 100644 index 00000000000..433badc2f16 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2beta1.json @@ -0,0 +1,208 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "dashboard-properties-test", + "labels": { + "test": "dashboard-properties" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": { + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "Simple Annotation", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "Testing dashboard-level property transformations including time settings, cursor sync, and links", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple Panel", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "", + "version": "v0", + "spec": {} + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [ + { + "title": "External Monitoring System", + "type": "link", + "icon": "external link", + "tooltip": "View in external system", + "url": "https://monitoring.example.com/dashboard?from=${__from}\u0026to=${__to}", + "tags": [ + "monitoring", + "alerts" + ], + "asDropdown": false, + "targetBlank": true, + "includeVars": true, + "keepTime": true + }, + { + "title": "Related Dashboards", + "type": "dashboards", + "icon": "dashboard", + "tooltip": "Navigate to related dashboards", + "url": "", + "tags": [ + "grafana", + "internal" + ], + "asDropdown": true, + "targetBlank": false, + "includeVars": false, + "keepTime": false + }, + { + "title": "Documentation", + "type": "link", + "icon": "info", + "tooltip": "View documentation", + "url": "https://docs.example.com/dashboard-guide", + "tags": [], + "asDropdown": false, + "targetBlank": true, + "includeVars": false, + "keepTime": false + }, + { + "title": "Tag-based Link", + "type": "tag", + "icon": "tag", + "tooltip": "Filtered by tags", + "url": "/dashboards/tag/alerts", + "tags": [ + "alerts" + ], + "asDropdown": false, + "targetBlank": false, + "includeVars": true, + "keepTime": true + } + ], + "liveNow": true, + "preload": true, + "tags": [ + "test", + "properties", + "metadata" + ], + "timeSettings": { + "timezone": "America/New_York", + "from": "now-12h", + "to": "now", + "autoRefresh": "30s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "sunday", + "fiscalYearStartMonth": 4, + "nowDelay": "1m" + }, + "title": "Dashboard Properties Test", + "variables": [ + { + "kind": "CustomVariable", + "spec": { + "name": "simple_var", + "query": "value1,value2,value3", + "current": { + "text": "value1", + "value": "value1" + }, + "options": [], + "multi": false, + "includeAll": false, + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v0alpha1.json new file mode 100644 index 00000000000..868eea5ca2e --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v0alpha1.json @@ -0,0 +1,353 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "panel-conversions-test", + "labels": { + "test": "panel-conversions" + } + }, + "spec": { + "annotations": { + "list": [] + }, + "description": "Testing panel to elements conversion", + "editable": true, + "links": [], + "panels": [ + { + "cacheTimeout": "60s", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "interval": "1m", + "links": [ + { + "targetBlank": true, + "title": "External Link", + "url": "https://example.com" + } + ], + "maxDataPoints": 300, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "expr": "up", + "hide": false, + "interval": "30s", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "expr": "cpu_usage", + "hide": true, + "interval": "1m", + "refId": "B" + } + ], + "title": "Regular Timeseries Panel", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": [ + "mean" + ] + } + } + ], + "transparent": true, + "type": "timeseries" + }, + { + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 2, + "libraryPanel": { + "name": "Shared Stats Panel", + "uid": "library-panel-uid-123" + }, + "title": "Library Panel Reference" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "edprtf91hz01se" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 3, + "panels": [], + "repeat": "server", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "edprtf91hz01se" + }, + "refId": "A" + } + ], + "title": "Row Panel", + "type": "row" + }, + { + "autoMigrateFrom": "graph", + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 9 + }, + "id": 4, + "maxPerRow": 4, + "repeat": "region", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "Panel in Row", + "type": "timeseries" + }, + { + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "orientation": "horizontal" + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 16, + "x": 8, + "y": 9 + }, + "hideTimeOverride": true, + "id": 7, + "links": [ + { + "title": "Drill Down", + "url": "/d/detailed-view" + } + ], + "maxDataPoints": 1000, + "options": { + "barWidth": 0.97, + "groupWidth": 0.7, + "orientation": "horizontal" + }, + "pluginVersion": "9.5.0", + "queryCachingTTL": 300, + "targets": [ + { + "bucketAggs": [ + { + "field": "@timestamp", + "id": "2", + "type": "date_histogram" + } + ], + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "query": "*", + "refId": "A" + } + ], + "timeFrom": "1h", + "timeShift": "1h", + "title": "Complex Panel with All Features", + "transformations": [ + { + "id": "groupBy", + "options": { + "fields": { + "status": { + "aggregations": [ + "count" + ], + "operation": "groupby" + } + } + } + }, + { + "id": "sortBy", + "options": { + "sort": [ + { + "desc": true, + "field": "count" + } + ] + } + } + ], + "type": "barchart" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "edprtf91hz01se" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 5, + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 6, + "options": { + "showHeader": true + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "format": "table", + "rawSql": "SELECT * FROM metrics", + "refId": "A" + } + ], + "title": "Hidden Panel in Collapsed Row", + "type": "table" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "refId": "A" + } + ], + "title": "Collapsed Row", + "type": "row" + } + ], + "preload": false, + "tags": [ + "test", + "panels" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "title": "Panel Conversions Test Dashboard" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2alpha1.json new file mode 100644 index 00000000000..a291ccbc6b2 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2alpha1.json @@ -0,0 +1,505 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "panel-conversions-test", + "labels": { + "test": "panel-conversions" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing panel to elements conversion", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Regular Timeseries Panel", + "description": "", + "links": [ + { + "title": "External Link", + "url": "https://example.com", + "targetBlank": true + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "up", + "interval": "30s" + } + }, + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "expr": "cpu_usage", + "interval": "1m" + } + }, + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "refId": "B", + "hidden": true + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": { + "maxDataPoints": 300, + "interval": "1m", + "cacheTimeout": "60s" + } + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "10.0.0", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + } + } + }, + "transparent": true + } + }, + "panel-2": { + "kind": "LibraryPanel", + "spec": { + "id": 2, + "title": "Library Panel Reference", + "libraryPanel": { + "name": "Shared Stats Panel", + "uid": "library-panel-uid-123" + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel in Row", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "scenarioId": "random_walk" + } + }, + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "", + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Hidden Panel in Collapsed Row", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "format": "table", + "rawSql": "SELECT * FROM metrics" + } + }, + "datasource": { + "type": "loki", + "uid": "non-default-test-ds-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "table", + "spec": { + "pluginVersion": "", + "options": { + "showHeader": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Complex Panel with All Features", + "description": "", + "links": [ + { + "title": "Drill Down", + "url": "/d/detailed-view" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "spec": { + "bucketAggs": [ + { + "field": "@timestamp", + "id": "2", + "type": "date_histogram" + } + ], + "query": "*" + } + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "groupBy", + "spec": { + "id": "groupBy", + "options": { + "fields": { + "status": { + "aggregations": [ + "count" + ], + "operation": "groupby" + } + } + } + } + }, + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "sort": [ + { + "desc": true, + "field": "count" + } + ] + } + } + } + ], + "queryOptions": { + "timeFrom": "1h", + "maxDataPoints": 1000, + "timeShift": "1h", + "queryCachingTTL": 300, + "hideTimeOverride": true + } + } + }, + "vizConfig": { + "kind": "barchart", + "spec": { + "pluginVersion": "9.5.0", + "options": { + "barWidth": 0.97, + "groupWidth": 0.7, + "orientation": "horizontal" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "orientation": "horizontal" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row Panel", + "collapse": false, + "repeat": { + "mode": "variable", + "value": "server" + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-4" + }, + "repeat": { + "mode": "variable", + "value": "region", + "direction": "h", + "maxPerRow": 4 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 16, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "panels" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-1h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Panel Conversions Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2beta1.json new file mode 100644 index 00000000000..8eac23d96c0 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2beta1.json @@ -0,0 +1,514 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "panel-conversions-test", + "labels": { + "test": "panel-conversions" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing panel to elements conversion", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Regular Timeseries Panel", + "description": "", + "links": [ + { + "title": "External Link", + "url": "https://example.com", + "targetBlank": true + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "existing-ref-uid" + }, + "spec": { + "expr": "up", + "interval": "30s" + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "existing-ref-uid" + }, + "spec": { + "expr": "cpu_usage", + "interval": "1m" + } + }, + "refId": "B", + "hidden": true + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": { + "maxDataPoints": 300, + "interval": "1m", + "cacheTimeout": "60s" + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "10.0.0", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": null, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + } + } + }, + "transparent": true + } + }, + "panel-2": { + "kind": "LibraryPanel", + "spec": { + "id": 2, + "title": "Library Panel Reference", + "libraryPanel": { + "name": "Shared Stats Panel", + "uid": "library-panel-uid-123" + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel in Row", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "default-ds-uid" + }, + "spec": { + "scenarioId": "random_walk" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "", + "spec": { + "options": {}, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Hidden Panel in Collapsed Row", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "loki", + "version": "v0", + "datasource": { + "name": "non-default-test-ds-uid" + }, + "spec": { + "format": "table", + "rawSql": "SELECT * FROM metrics" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "table", + "version": "", + "spec": { + "options": { + "showHeader": true + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + } + } + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "id": 7, + "title": "Complex Panel with All Features", + "description": "", + "links": [ + { + "title": "Drill Down", + "url": "/d/detailed-view" + } + ], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": { + "bucketAggs": [ + { + "field": "@timestamp", + "id": "2", + "type": "date_histogram" + } + ], + "query": "*" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "groupBy", + "spec": { + "id": "groupBy", + "options": { + "fields": { + "status": { + "aggregations": [ + "count" + ], + "operation": "groupby" + } + } + } + } + }, + { + "kind": "sortBy", + "spec": { + "id": "sortBy", + "options": { + "sort": [ + { + "desc": true, + "field": "count" + } + ] + } + } + } + ], + "queryOptions": { + "timeFrom": "1h", + "maxDataPoints": 1000, + "timeShift": "1h", + "queryCachingTTL": 300, + "hideTimeOverride": true + } + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "barchart", + "version": "9.5.0", + "spec": { + "options": { + "barWidth": 0.97, + "groupWidth": 0.7, + "orientation": "horizontal" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "orientation": "horizontal" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "title": "", + "collapse": false, + "hideHeader": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 12, + "y": 0, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Row Panel", + "collapse": false, + "repeat": { + "mode": "variable", + "value": "server" + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 8, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-4" + }, + "repeat": { + "mode": "variable", + "value": "region", + "direction": "h", + "maxPerRow": 4 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 8, + "y": 0, + "width": 16, + "height": 6, + "element": { + "kind": "ElementReference", + "name": "panel-7" + } + } + } + ] + } + } + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "title": "Collapsed Row", + "collapse": true, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "panels" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-1h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Panel Conversions Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json new file mode 100644 index 00000000000..e4a5b7b74eb --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json @@ -0,0 +1,360 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "variable-conversions-test", + "labels": { + "test": "variable-conversions" + } + }, + "spec": { + "annotations": { + "list": [] + }, + "description": "Testing all variable types and transformations", + "links": [], + "panels": [], + "tags": [ + "test", + "variables" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus-uid" + }, + "description": "Select a data source", + "hide": 0, + "includeAll": false, + "label": "Data Source", + "multi": false, + "name": "datasource_var", + "options": [ + { + "selected": true, + "text": "Prometheus", + "value": "prometheus-uid" + }, + { + "selected": false, + "text": "InfluxDB", + "value": "influx-uid" + } + ], + "query": "prometheus", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All", + "localhost:9090" + ], + "value": [ + "$__all", + "localhost:9090" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "definition": "label_values(up, instance)", + "description": "Available instances", + "hide": 1, + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "query_var", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "localhost:9090", + "value": "localhost:9090" + }, + { + "selected": false, + "text": "localhost:9091", + "value": "localhost:9091" + } + ], + "query": "label_values(up, instance)", + "refresh": 2, + "regex": "/.*9090.*/", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "selected": false, + "text": "prod", + "value": "prod" + }, + "description": "Deployment environment", + "hide": 0, + "includeAll": true, + "label": "Environment", + "multi": false, + "name": "custom_var", + "options": [ + { + "selected": true, + "text": "prod", + "value": "prod" + }, + { + "selected": false, + "text": "staging", + "value": "staging" + }, + { + "selected": false, + "text": "dev", + "value": "dev" + } + ], + "query": "prod,staging,dev", + "skipUrlSync": false, + "type": "custom" + }, + { + "allowCustomValue": true, + "baseFilters": [ + { + "key": "environment", + "operator": "=", + "value": "production" + } + ], + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "defaultKeys": [ + { + "text": "job", + "value": "job" + }, + { + "text": "instance", + "value": "instance" + } + ], + "description": "Add filters", + "filters": [ + { + "key": "job", + "operator": "=", + "value": "prometheus" + } + ], + "hide": 0, + "label": "Filters", + "name": "adhoc_var", + "skipUrlSync": false, + "type": "adhoc" + }, + { + "current": { + "selected": false, + "text": "my-application", + "value": "my-application" + }, + "description": "Application name constant", + "hide": 2, + "label": "App Name", + "name": "constant_var", + "query": "my-application", + "skipUrlSync": true, + "type": "constant" + }, + { + "auto": true, + "auto_count": 30, + "auto_min": "10s", + "current": { + "selected": false, + "text": "5m", + "value": "5m" + }, + "description": "Time interval for queries", + "hide": 0, + "label": "Interval", + "name": "interval_var", + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": true, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + } + ], + "query": "1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + }, + { + "current": { + "selected": false, + "text": "error", + "value": "error" + }, + "description": "Enter search term", + "hide": 0, + "label": "Search Term", + "name": "textbox_var", + "query": "default_search", + "skipUrlSync": false, + "type": "textbox" + }, + { + "current": { + "selected": true, + "text": [ + "job", + "instance" + ], + "value": [ + "job", + "instance" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "description": "Group results by field", + "hide": 0, + "label": "Group By", + "multi": true, + "name": "groupby_var", + "options": [ + { + "selected": true, + "text": "job", + "value": "job" + }, + { + "selected": true, + "text": "instance", + "value": "instance" + }, + { + "selected": false, + "text": "region", + "value": "region" + } + ], + "query": "", + "skipUrlSync": false, + "type": "groupby" + }, + { + "current": { + "selected": false, + "text": "legacy_value", + "value": "legacy_string_content" + }, + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "description": "Variable with legacy string value", + "hide": 0, + "label": "Legacy Variable", + "name": "legacy_string_var", + "query": "up", + "refresh": 1, + "skipUrlSync": false, + "type": "query" + }, + { + "allValue": "*", + "current": { + "selected": true, + "text": [ + "All", + "host1", + "host2" + ], + "value": [ + "$__all", + "host1", + "host2" + ] + }, + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "definition": "terms field:@host size:100", + "description": "Variable with complex query and all features", + "hide": 0, + "includeAll": true, + "label": "Complex Query Variable", + "multi": true, + "name": "complex_query_var", + "options": [], + "query": { + "bucketAggs": [ + { + "field": "@host", + "id": "2", + "type": "terms" + } + ], + "query": "*", + "size": 100 + }, + "refresh": 1, + "regex": "/host[0-9]+/", + "skipUrlSync": false, + "sort": 2, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": true + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "title": "Variable Conversions Test Dashboard" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json new file mode 100644 index 00000000000..556c3774d34 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json @@ -0,0 +1,403 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "variable-conversions-test", + "labels": { + "test": "variable-conversions" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing all variable types and transformations", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "variables" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Variable Conversions Test Dashboard", + "variables": [ + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource_var", + "pluginId": "prometheus", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "Prometheus", + "value": "prometheus-uid" + }, + "options": [ + { + "selected": true, + "text": "Prometheus", + "value": "prometheus-uid" + }, + { + "selected": false, + "text": "InfluxDB", + "value": "influx-uid" + } + ], + "multi": false, + "includeAll": false, + "label": "Data Source", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a data source", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "query_var", + "current": { + "text": [ + "All", + "localhost:9090" + ], + "value": [ + "$__all", + "localhost:9090" + ] + }, + "label": "Instance", + "hide": "hideLabel", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "description": "Available instances", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "/.*9090.*/", + "sort": "alphabeticalAsc", + "definition": "label_values(up, instance)", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "localhost:9090", + "value": "localhost:9090" + }, + { + "selected": false, + "text": "localhost:9091", + "value": "localhost:9091" + } + ], + "multi": true, + "includeAll": true, + "allValue": ".*", + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod,staging,dev", + "current": { + "text": "prod", + "value": "prod" + }, + "options": [ + { + "selected": true, + "text": "prod", + "value": "prod" + }, + { + "selected": false, + "text": "staging", + "value": "staging" + }, + { + "selected": false, + "text": "dev", + "value": "dev" + } + ], + "multi": false, + "includeAll": true, + "label": "Environment", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Deployment environment", + "allowCustomValue": true + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "adhoc_var", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "baseFilters": [ + { + "key": "environment", + "operator": "=", + "value": "production" + } + ], + "filters": [ + { + "key": "job", + "operator": "=", + "value": "prometheus" + } + ], + "defaultKeys": [ + { + "text": "job", + "value": "job" + }, + { + "text": "instance", + "value": "instance" + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Add filters", + "allowCustomValue": true + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "my-application", + "current": { + "text": "my-application", + "value": "my-application" + }, + "label": "App Name", + "hide": "hideVariable", + "skipUrlSync": true, + "description": "Application name constant" + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval_var", + "query": "1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "current": { + "text": "5m", + "value": "5m" + }, + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": true, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "onTimeRangeChanged", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval for queries" + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "textbox_var", + "current": { + "text": "error", + "value": "error" + }, + "query": "default_search", + "label": "Search Term", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Enter search term" + } + }, + { + "kind": "GroupByVariable", + "spec": { + "name": "groupby_var", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "current": { + "text": [ + "job", + "instance" + ], + "value": [ + "job", + "instance" + ] + }, + "options": [ + { + "selected": true, + "text": "job", + "value": "job" + }, + { + "selected": true, + "text": "instance", + "value": "instance" + }, + { + "selected": false, + "text": "region", + "value": "region" + } + ], + "multi": true, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group results by field" + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "legacy_string_var", + "current": { + "text": "legacy_value", + "value": "legacy_string_content" + }, + "label": "Legacy Variable", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "description": "Variable with legacy string value", + "datasource": { + "type": "prometheus", + "uid": "existing-ref-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "__legacyStringValue": "up" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "complex_query_var", + "current": { + "text": [ + "All", + "host1", + "host2" + ], + "value": [ + "$__all", + "host1", + "host2" + ] + }, + "label": "Complex Query Variable", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "description": "Variable with complex query and all features", + "datasource": { + "type": "elasticsearch", + "uid": "existing-target-uid" + }, + "query": { + "kind": "DataQuery", + "spec": { + "bucketAggs": [ + { + "field": "@host", + "id": "2", + "type": "terms" + } + ], + "query": "*", + "size": 100 + } + }, + "regex": "/host[0-9]+/", + "sort": "alphabeticalDesc", + "definition": "terms field:@host size:100", + "options": [], + "multi": true, + "includeAll": true, + "allValue": "*", + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json new file mode 100644 index 00000000000..d35e57fa601 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json @@ -0,0 +1,406 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "variable-conversions-test", + "labels": { + "test": "variable-conversions" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing all variable types and transformations", + "editable": true, + "elements": {}, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "variables" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Variable Conversions Test Dashboard", + "variables": [ + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasource_var", + "pluginId": "prometheus", + "refresh": "onDashboardLoad", + "regex": "", + "current": { + "text": "Prometheus", + "value": "prometheus-uid" + }, + "options": [ + { + "selected": true, + "text": "Prometheus", + "value": "prometheus-uid" + }, + { + "selected": false, + "text": "InfluxDB", + "value": "influx-uid" + } + ], + "multi": false, + "includeAll": false, + "label": "Data Source", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a data source", + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "query_var", + "current": { + "text": [ + "All", + "localhost:9090" + ], + "value": [ + "$__all", + "localhost:9090" + ] + }, + "label": "Instance", + "hide": "hideLabel", + "refresh": "onTimeRangeChanged", + "skipUrlSync": false, + "description": "Available instances", + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "existing-ref-uid" + }, + "spec": { + "__legacyStringValue": "label_values(up, instance)" + } + }, + "regex": "/.*9090.*/", + "sort": "alphabeticalAsc", + "definition": "label_values(up, instance)", + "options": [ + { + "selected": false, + "text": "All", + "value": "$__all" + }, + { + "selected": true, + "text": "localhost:9090", + "value": "localhost:9090" + }, + { + "selected": false, + "text": "localhost:9091", + "value": "localhost:9091" + } + ], + "multi": true, + "includeAll": true, + "allValue": ".*", + "allowCustomValue": true + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod,staging,dev", + "current": { + "text": "prod", + "value": "prod" + }, + "options": [ + { + "selected": true, + "text": "prod", + "value": "prod" + }, + { + "selected": false, + "text": "staging", + "value": "staging" + }, + { + "selected": false, + "text": "dev", + "value": "dev" + } + ], + "multi": false, + "includeAll": true, + "label": "Environment", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Deployment environment", + "allowCustomValue": true + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "existing-ref-uid" + }, + "spec": { + "name": "adhoc_var", + "baseFilters": [ + { + "key": "environment", + "operator": "=", + "value": "production" + } + ], + "filters": [ + { + "key": "job", + "operator": "=", + "value": "prometheus" + } + ], + "defaultKeys": [ + { + "text": "job", + "value": "job" + }, + { + "text": "instance", + "value": "instance" + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Add filters", + "allowCustomValue": true + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "my-application", + "current": { + "text": "my-application", + "value": "my-application" + }, + "label": "App Name", + "hide": "hideVariable", + "skipUrlSync": true, + "description": "Application name constant" + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval_var", + "query": "1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "current": { + "text": "5m", + "value": "5m" + }, + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": true, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "onTimeRangeChanged", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval for queries" + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "textbox_var", + "current": { + "text": "error", + "value": "error" + }, + "query": "default_search", + "label": "Search Term", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Enter search term" + } + }, + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "existing-ref-uid" + }, + "spec": { + "name": "groupby_var", + "current": { + "text": [ + "job", + "instance" + ], + "value": [ + "job", + "instance" + ] + }, + "options": [ + { + "selected": true, + "text": "job", + "value": "job" + }, + { + "selected": true, + "text": "instance", + "value": "instance" + }, + { + "selected": false, + "text": "region", + "value": "region" + } + ], + "multi": true, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group results by field" + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "legacy_string_var", + "current": { + "text": "legacy_value", + "value": "legacy_string_content" + }, + "label": "Legacy Variable", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "description": "Variable with legacy string value", + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "existing-ref-uid" + }, + "spec": { + "__legacyStringValue": "up" + } + }, + "regex": "", + "sort": "disabled", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "complex_query_var", + "current": { + "text": [ + "All", + "host1", + "host2" + ], + "value": [ + "$__all", + "host1", + "host2" + ] + }, + "label": "Complex Query Variable", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "description": "Variable with complex query and all features", + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "existing-target-uid" + }, + "spec": { + "bucketAggs": [ + { + "field": "@host", + "id": "2", + "type": "terms" + } + ], + "query": "*", + "size": 100 + } + }, + "regex": "/host[0-9]+/", + "sort": "alphabeticalDesc", + "definition": "terms field:@host size:100", + "options": [], + "multi": true, + "includeAll": true, + "allValue": "*", + "allowCustomValue": true + } + } + ] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json index a7fa8532074..6d044300534 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json @@ -79,7 +79,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "datasource": { "name": "gdev-prometheus" @@ -399,7 +399,7 @@ "auto": true, "auto_min": "10s", "auto_count": 30, - "refresh": "load", + "refresh": "onTimeRangeChanged", "label": "Interval", "hide": "dontHide", "skipUrlSync": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json index e0efe19ed2b..7e8f0151ebb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json @@ -58,7 +58,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "spec": { "lines": 10, @@ -103,7 +103,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "spec": { "expr": "{action=\"add_client\"}", @@ -213,7 +213,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "grafana-testdata-datasource", + "group": "", "version": "v0", "datasource": { "name": "gdev-testdata" @@ -324,7 +324,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "grafana-testdata-datasource", + "group": "", "version": "v0", "spec": { "scenarioId": "random_walk", @@ -403,7 +403,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "spec": { "disableTextWrap": false, @@ -518,7 +518,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "datasource": { "name": "gdev-prometheus" @@ -636,7 +636,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "datasource": { "name": "gdev-prometheus" @@ -661,7 +661,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "grafana-testdata-datasource", + "group": "", "version": "v0", "datasource": { "name": "gdev-testdata" @@ -769,7 +769,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "spec": { "disableTextWrap": false, @@ -791,7 +791,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "grafana-testdata-datasource", + "group": "", "version": "v0", "spec": {} }, @@ -984,7 +984,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "datasource": { "name": "gdev-prometheus" @@ -1014,7 +1014,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "grafana-testdata-datasource", + "group": "", "version": "v0", "spec": { "csv": "1,2,3,4", @@ -1042,7 +1042,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "grafana-testdata-datasource", + "group": "", "version": "v0", "spec": {} }, @@ -1067,7 +1067,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "prometheus", + "group": "", "version": "v0", "spec": { "expr": "up" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json index a7f48d67563..ae0dc06ab00 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json @@ -24,7 +24,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "grafana-testdata-datasource", + "group": "", "version": "v0", "datasource": { "name": "gdev-testdata" diff --git a/apps/dashboard/pkg/migration/conversion/v0.go b/apps/dashboard/pkg/migration/conversion/v0.go index 9eb42167072..81ea2db5e60 100644 --- a/apps/dashboard/pkg/migration/conversion/v0.go +++ b/apps/dashboard/pkg/migration/conversion/v0.go @@ -1,10 +1,6 @@ package conversion import ( - "context" - - "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/identity" "k8s.io/apimachinery/pkg/conversion" "k8s.io/utils/ptr" @@ -12,91 +8,104 @@ import ( dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" - "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" - "k8s.io/apiserver/pkg/endpoints/request" ) -func Convert_V0_to_V1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - out.Spec.Object = in.Spec.Object - - out.Status = dashv1.DashboardStatus{ - Conversion: &dashv1.DashboardConversionStatus{ - StoredVersion: ptr.To(dashv0.VERSION), - }, - } - - // the scope passed into this function is used in k8s apimachinery for migrations, but we also need the context - // to have what grafana expects in the request context, so that we can retrieve datasources for migrating - // some of the old dashboard schemas (these migrations used to be run in the frontend) - ctx := request.WithNamespace(context.Background(), in.GetNamespace()) - nsInfo, err := types.ParseNamespace(in.GetNamespace()) - if err != nil { - out.Status.Conversion.Failed = true - out.Status.Conversion.Error = ptr.To(err.Error()) - return schemaversion.NewMigrationError(err.Error(), schemaversion.GetSchemaVersion(in.Spec.Object), schemaversion.LATEST_VERSION, "Convert_V0_to_V1") - } - - // a background service identity is used here because the user who is reading the specific dashboard - // may not have access to all the datasources in the dashboard, but the migration still needs to take place - // in order to be able to convert between k8s versions (so that we have a guaranteed structure to convert between) - ctx, _ = identity.WithServiceIdentity(ctx, nsInfo.OrgID) - - if err := migration.Migrate(ctx, out.Spec.Object, schemaversion.LATEST_VERSION); err != nil { - out.Status.Conversion.Failed = true - out.Status.Conversion.Error = ptr.To(err.Error()) - return schemaversion.NewMigrationError(err.Error(), schemaversion.GetSchemaVersion(in.Spec.Object), schemaversion.LATEST_VERSION, "Convert_V0_to_V1") - } - - return nil -} - -func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - // TODO (@radiohead): implement V0 to V2 conversion - // This is the bare minimum conversion that is needed to make the dashboard servable. - - if v, ok := in.Spec.Object["title"]; ok { - if title, ok := v.(string); ok { - out.Spec.Title = title +func Convert_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { + if err := ConvertDashboard_V0_to_V1beta1(in, out, scope); err != nil { + out.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, } + return err + } + return nil +} + +func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsInfoProvider schemaversion.DataSourceInfoProvider) error { + v1beta1 := &dashv1.Dashboard{} + if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil { + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, + } + // Don't return error - just set status (matches test expectations) + // Ensure layout is set even on error to prevent JSON marshaling issues + if out.Spec.Layout.GridLayoutKind == nil && out.Spec.Layout.RowsLayoutKind == nil { + out.Spec.Layout = dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{}, + }, + } + } + return nil } - // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. - out.Spec.Layout = dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ - GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ - Kind: "GridLayout", - Spec: dashv2alpha1.DashboardGridLayoutSpec{}, - }, - } - - out.Status = dashv2alpha1.DashboardStatus{ - Conversion: &dashv2alpha1.DashboardConversionStatus{ - StoredVersion: ptr.To(dashv0.VERSION), - Failed: true, - Error: ptr.To("backend conversion not yet implemented"), - Source: in, - }, + if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, out, scope, dsInfoProvider); err != nil { + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, + } + // Don't return error - just set status (matches test expectations) + // Ensure layout is set even on error to prevent JSON marshaling issues + if out.Spec.Layout.GridLayoutKind == nil && out.Spec.Layout.RowsLayoutKind == nil { + out.Spec.Layout = dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{}, + }, + } + } + return nil } return nil } -func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta +func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsInfoProvider schemaversion.DataSourceInfoProvider) error { + v1beta1 := &dashv1.Dashboard{} + if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil { + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, + } + return err + } - // TODO: implement V0 to v2beta1 conversion + v2alpha1 := &dashv2alpha1.Dashboard{} + if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, v2alpha1, scope, dsInfoProvider); err != nil { + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, + } + return err + } - out.Status = dashv2beta1.DashboardStatus{ - Conversion: &dashv2beta1.DashboardConversionStatus{ - StoredVersion: ptr.To(dashv0.VERSION), - Failed: true, - Error: ptr.To("backend conversion not yet implemented"), - Source: in, - }, + if err := ConvertDashboard_V2alpha1_to_V2beta1(v2alpha1, out, scope); err != nil { + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, + } + return err } return nil diff --git a/apps/dashboard/pkg/migration/conversion/v0_test.go b/apps/dashboard/pkg/migration/conversion/v0_test.go new file mode 100644 index 00000000000..40f118dc9f2 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v0_test.go @@ -0,0 +1,392 @@ +package conversion + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// TestV0ConversionErrorHandling tests that v0 conversion functions properly return errors and set status +func TestV0ConversionErrorHandling(t *testing.T) { + // Initialize the migrator with a test data source provider + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) + + tests := []struct { + name string + source *dashv0.Dashboard + target interface{} + expectError bool + expectStatusSet bool + checkStatus func(t *testing.T, target interface{}) + }{ + { + name: "Convert_V0_to_V1beta1 returns error on migration failure", + source: &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, // Invalid schema version that will cause migration to fail + }, + }, + }, + target: &dashv1.Dashboard{}, + expectError: true, + expectStatusSet: true, + checkStatus: func(t *testing.T, target interface{}) { + out := target.(*dashv1.Dashboard) + require.NotNil(t, out.Status.Conversion) + require.True(t, out.Status.Conversion.Failed) + require.NotNil(t, out.Status.Conversion.Error) + }, + }, + { + name: "Convert_V0_to_V2alpha1 sets status on first step migration failure", + source: &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, // Invalid schema version that will cause migration to fail + }, + }, + }, + target: &dashv2alpha1.Dashboard{}, + expectError: false, // Convert_V0_to_V2alpha1 doesn't return error, just sets status + expectStatusSet: true, + checkStatus: func(t *testing.T, target interface{}) { + out := target.(*dashv2alpha1.Dashboard) + // Status should be set when first step fails + require.NotNil(t, out.Status.Conversion) + require.True(t, out.Status.Conversion.Failed) + require.NotNil(t, out.Status.Conversion.Error) + }, + }, + { + name: "Convert_V0_to_V2beta1 returns error on first step migration failure", + source: &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, // Invalid schema version that will cause migration to fail + }, + }, + }, + target: &dashv2beta1.Dashboard{}, + expectError: true, + expectStatusSet: true, + checkStatus: func(t *testing.T, target interface{}) { + out := target.(*dashv2beta1.Dashboard) + require.NotNil(t, out.Status.Conversion) + require.True(t, out.Status.Conversion.Failed) + require.NotNil(t, out.Status.Conversion.Error) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var err error + switch target := tt.target.(type) { + case *dashv1.Dashboard: + err = Convert_V0_to_V1beta1(tt.source, target, nil) + case *dashv2alpha1.Dashboard: + err = Convert_V0_to_V2alpha1(tt.source, target, nil, dsProvider) + case *dashv2beta1.Dashboard: + err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider) + default: + t.Fatalf("unexpected target type: %T", target) + } + + if tt.expectError { + require.Error(t, err, "expected conversion to return error") + } + + if tt.expectStatusSet { + tt.checkStatus(t, tt.target) + } + }) + } +} + +// TestV0ConversionErrorPropagation tests that errors from atomic functions are properly propagated +func TestV0ConversionErrorPropagation(t *testing.T) { + // Initialize the migrator with a test data source provider + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) + + t.Run("ConvertDashboard_V0_to_V1beta1 returns error on migration failure", func(t *testing.T) { + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, // Invalid schema version that will cause migration to fail + }, + }, + } + target := &dashv1.Dashboard{} + + err := ConvertDashboard_V0_to_V1beta1(source, target, nil) + + require.Error(t, err, "expected error on migration failure") + require.NotNil(t, target.Status.Conversion) + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + }) + + t.Run("Convert_V0_to_V1beta1 propagates error from ConvertDashboard_V0_to_V1beta1", func(t *testing.T) { + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, // Invalid schema version that will cause migration to fail + }, + }, + } + target := &dashv1.Dashboard{} + + err := Convert_V0_to_V1beta1(source, target, nil) + + require.Error(t, err, "expected error to be returned") + require.NotNil(t, target.Status.Conversion) + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + }) + + t.Run("Convert_V0_to_V2beta1 returns error on first step migration failure", func(t *testing.T) { + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, // Invalid schema version that will cause migration to fail + }, + }, + } + target := &dashv2beta1.Dashboard{} + + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + + require.Error(t, err, "expected error to be returned on first step failure") + require.NotNil(t, target.Status.Conversion) + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + }) +} + +// TestV0ConversionSuccessPaths tests that successful conversion paths are covered +func TestV0ConversionSuccessPaths(t *testing.T) { + // Initialize the migrator with a test data source provider + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) + + t.Run("Convert_V0_to_V1beta1 success path returns nil", func(t *testing.T) { + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, // Latest schema version - no migration needed + }, + }, + } + target := &dashv1.Dashboard{} + + err := Convert_V0_to_V1beta1(source, target, nil) + + require.NoError(t, err, "expected successful conversion") + require.NotNil(t, target.Status.Conversion) + require.False(t, target.Status.Conversion.Failed) + }) + + t.Run("Convert_V0_to_V2alpha1 success path returns nil", func(t *testing.T) { + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, // Latest schema version - no migration needed + }, + }, + } + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + + require.NoError(t, err, "expected successful conversion") + // Layout should be set even on success + require.NotNil(t, target.Spec.Layout.GridLayoutKind) + }) + + t.Run("Convert_V0_to_V2beta1 success path returns nil", func(t *testing.T) { + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, // Latest schema version - no migration needed + }, + }, + } + target := &dashv2beta1.Dashboard{} + + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + + require.NoError(t, err, "expected successful conversion") + }) +} + +// TestV0ConversionSecondStepErrors tests error handling in second step of multi-step conversions +func TestV0ConversionSecondStepErrors(t *testing.T) { + // Initialize the migrator with a test data source provider + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) + + t.Run("Convert_V0_to_V2alpha1 sets status on first step error", func(t *testing.T) { + // Create a dashboard that will fail v0->v1beta1 conversion + // Use schemaVersion 0 which will cause migration to fail + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 0, // Invalid schema version that will cause migration to fail + }, + }, + } + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + + // Convert_V0_to_V2alpha1 doesn't return error, just sets status + require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error") + // Status should be set when first step fails + require.NotNil(t, target.Status.Conversion, "Status should be set on first step error") + require.True(t, target.Status.Conversion.Failed, "Failed should be true") + require.NotNil(t, target.Status.Conversion.Error, "Error should be set") + require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion) + }) + + t.Run("Convert_V0_to_V2alpha1 sets status on second step error", func(t *testing.T) { + // Create a dashboard that will pass v0->v1beta1 but fail v1beta1->v2alpha1 + // We need to create invalid JSON structure that will cause JSON marshaling to fail + // or create a dashboard with invalid structure that causes transformation to fail + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 42, // Valid schema version that will pass first step + "dashboard": map[string]interface{}{ + "title": "test", + // Create invalid structure that will cause JSON marshaling to fail + // by using a channel which cannot be marshaled + "invalidField": make(chan int), // This will cause JSON marshal to fail + }, + }, + }, + } + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + + // Convert_V0_to_V2alpha1 doesn't return error, just sets status + require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error") + // If second step fails, status should be set + // Note: The error might occur in first step if JSON marshal fails early, + // but we're testing that the error handling path exists + if target.Status.Conversion != nil && target.Status.Conversion.Failed { + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion) + } + }) + + t.Run("Convert_V0_to_V2beta1 returns error on second step failure", func(t *testing.T) { + // Create a scenario where first step succeeds but second step fails + // This is harder to trigger, but we can test the code path exists + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 25, // Valid schema version that will pass first step + }, + }, + } + target := &dashv2beta1.Dashboard{} + + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + + // May or may not error depending on dashboard content + // But if it does error on second step, status should be set + if err != nil { + require.NotNil(t, target.Status.Conversion) + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + } + }) + + t.Run("Convert_V0_to_V2beta1 returns error on third step failure", func(t *testing.T) { + // Create a scenario where first two steps succeed but third step fails + source := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 25, // Valid schema version that will pass first two steps + }, + }, + } + target := &dashv2beta1.Dashboard{} + + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + + // May or may not error depending on dashboard content + // But if it does error on third step, status should be set + if err != nil { + require.NotNil(t, target.Status.Conversion) + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + } + }) +} diff --git a/apps/dashboard/pkg/migration/conversion/v0alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v0alpha1_to_v1beta1.go new file mode 100644 index 00000000000..3e78efdcb29 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v0alpha1_to_v1beta1.go @@ -0,0 +1,81 @@ +package conversion + +import ( + "context" + + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/utils/ptr" + + "github.com/grafana/authlib/types" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +// prepareV0ConversionContext sets up the context with namespace and service identity +// for v0 dashboard conversions. This context is needed to retrieve datasources for migrating +// old dashboard schemas (these migrations used to be run in the frontend). +// A background service identity is used because the user who is reading the specific dashboard +// may not have access to all the datasources in the dashboard, but the migration still needs to take place +// in order to be able to convert between k8s versions (so that we have a guaranteed structure to convert between). +func prepareV0ConversionContext(in *dashv0.Dashboard) (context.Context, *types.NamespaceInfo, error) { + ctx := request.WithNamespace(context.Background(), in.GetNamespace()) + nsInfo, err := types.ParseNamespace(in.GetNamespace()) + if err != nil { + return nil, nil, err + } + + ctx, _ = identity.WithServiceIdentity(ctx, nsInfo.OrgID) + return ctx, &nsInfo, nil +} + +// migrateV0Dashboard migrates a v0 dashboard object to the target schema version. +// This is used to ensure the dashboard structure is consistent before converting between k8s API versions. +func migrateV0Dashboard(ctx context.Context, dashboardObject map[string]interface{}, targetVersion int) error { + if ctx == nil { + return schemaversion.NewMigrationError("context is nil", schemaversion.GetSchemaVersion(dashboardObject), targetVersion, "migrateV0Dashboard") + } + return migration.Migrate(ctx, dashboardObject, targetVersion) +} + +// ConvertDashboard_V0_to_V1beta1 converts a v0alpha1 dashboard to v1beta1 format. +// This is an atomic single-step conversion that: +// 1. Migrates the dashboard to the latest schema version +// 2. Transforms the migrated dashboard to v1beta1 format +// +// the scope passed into this function is used in k8s apimachinery for migrations, but we also need the context +// to have what grafana expects in the request context, so that we can retrieve datasources for migrating +// some of the old dashboard schemas (these migrations used to be run in the frontend) +// +// a background service identity is used here because the user who is reading the specific dashboard +// may not have access to all the datasources in the dashboard, but the migration still needs to take place +// in order to be able to convert between k8s versions (so that we have a guaranteed structure to convert between) +func ConvertDashboard_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + out.Spec.Object = in.Spec.Object + + out.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv0.VERSION), + }, + } + + ctx, _, err := prepareV0ConversionContext(in) + if err != nil { + out.Status.Conversion.Failed = true + out.Status.Conversion.Error = ptr.To(err.Error()) + return schemaversion.NewMigrationError(err.Error(), schemaversion.GetSchemaVersion(in.Spec.Object), schemaversion.LATEST_VERSION, "Convert_V0_to_V1") + } + + if err := migrateV0Dashboard(ctx, out.Spec.Object, schemaversion.LATEST_VERSION); err != nil { + out.Status.Conversion.Failed = true + out.Status.Conversion.Error = ptr.To(err.Error()) + return schemaversion.NewMigrationError(err.Error(), schemaversion.GetSchemaVersion(in.Spec.Object), schemaversion.LATEST_VERSION, "Convert_V0_to_V1") + } + + return nil +} diff --git a/apps/dashboard/pkg/migration/conversion/v1.go b/apps/dashboard/pkg/migration/conversion/v1.go index 86b3827af27..107d3010a9a 100644 --- a/apps/dashboard/pkg/migration/conversion/v1.go +++ b/apps/dashboard/pkg/migration/conversion/v1.go @@ -8,9 +8,10 @@ import ( dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) -func Convert_V1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { +func Convert_V1beta1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta out.Spec.Object = in.Spec.Object @@ -24,50 +25,63 @@ func Convert_V1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope convers return nil } -func Convert_V1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - // TODO (@radiohead): implement V1 to V2 conversion - // This is the bare minimum conversion that is needed to make the dashboard servable. - - if v, ok := in.Spec.Object["title"]; ok { - if title, ok := v.(string); ok { - out.Spec.Title = title +func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsInfoProvider schemaversion.DataSourceInfoProvider) error { + if err := ConvertDashboard_V1beta1_to_V2alpha1(in, out, scope, dsInfoProvider); err != nil { + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv1.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, } + // Don't return error - just set status (matches test expectations and V0 pattern for Convert_V0_to_V2alpha1) + // Ensure layout is set even on error to prevent JSON marshaling issues + if out.Spec.Layout.GridLayoutKind == nil && out.Spec.Layout.RowsLayoutKind == nil { + out.Spec.Layout = dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{}, + }, + } + } + return nil } // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. - out.Spec.Layout = dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ - GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ - Kind: "GridLayout", - Spec: dashv2alpha1.DashboardGridLayoutSpec{}, - }, - } - - out.Status = dashv2alpha1.DashboardStatus{ - Conversion: &dashv2alpha1.DashboardConversionStatus{ - StoredVersion: ptr.To(dashv1.VERSION), - Failed: true, - Error: ptr.To("backend conversion not yet implemented"), - Source: in, - }, + if out.Spec.Layout.GridLayoutKind == nil && out.Spec.Layout.RowsLayoutKind == nil { + out.Spec.Layout = dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{}, + }, + } } return nil } -func Convert_V1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta +func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsInfoProvider schemaversion.DataSourceInfoProvider) error { + v2alpha1 := &dashv2alpha1.Dashboard{} + if err := ConvertDashboard_V1beta1_to_V2alpha1(in, v2alpha1, scope, dsInfoProvider); err != nil { + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv1.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, + } + return err + } - // TODO: implement V1 to v2beta1 conversion - - out.Status = dashv2beta1.DashboardStatus{ - Conversion: &dashv2beta1.DashboardConversionStatus{ - StoredVersion: ptr.To(dashv1.VERSION), - Failed: true, - Error: ptr.To("backend conversion not yet implemented"), - Source: in, - }, + if err := ConvertDashboard_V2alpha1_to_V2beta1(v2alpha1, out, scope); err != nil { + out.Status = dashv2beta1.DashboardStatus{ + Conversion: &dashv2beta1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv1.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + }, + } + return err } return nil diff --git a/apps/dashboard/pkg/migration/conversion/v1_test.go b/apps/dashboard/pkg/migration/conversion/v1_test.go new file mode 100644 index 00000000000..c62d5e60d34 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v1_test.go @@ -0,0 +1,127 @@ +package conversion + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// TestV1ConversionErrorHandling tests that v1 conversion functions properly handle errors +func TestV1ConversionErrorHandling(t *testing.T) { + // Initialize the migrator with a test data source provider + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + migration.Initialize(dsProvider) + + t.Run("Convert_V1beta1_to_V2alpha1 sets status on conversion error", func(t *testing.T) { + // Create a dashboard that will cause conversion to fail + // We can use an invalid dashboard structure + source := &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + // Missing required fields that might cause conversion to fail + }, + }, + } + target := &dashv2alpha1.Dashboard{} + + err := Convert_V1beta1_to_V2alpha1(source, target, nil, dsProvider) + + // Convert_V1beta1_to_V2alpha1 doesn't return error, just sets status + require.NoError(t, err, "Convert_V1beta1_to_V2alpha1 doesn't return error") + // Layout should always be set + require.NotNil(t, target.Spec.Layout.GridLayoutKind) + // If conversion failed, status should be set + if target.Status.Conversion != nil { + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + } + }) + + t.Run("Convert_V1beta1_to_V2beta1 returns error on first step failure", func(t *testing.T) { + source := &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + // Missing required fields that might cause conversion to fail + }, + }, + } + target := &dashv2beta1.Dashboard{} + + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + + // May or may not error depending on dashboard content + // But if it does error on first step, status should be set with correct StoredVersion + if err != nil { + require.NotNil(t, target.Status.Conversion) + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + require.Equal(t, dashv1.VERSION, *target.Status.Conversion.StoredVersion) + } + }) + + t.Run("Convert_V1beta1_to_V2beta1 returns error on second step failure", func(t *testing.T) { + // Create a dashboard that will pass first step but might fail second step + source := &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 25, // Valid schema version + }, + }, + } + target := &dashv2beta1.Dashboard{} + + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + + // May or may not error depending on dashboard content + // But if it does error on second step, status should be set with correct StoredVersion + if err != nil { + require.NotNil(t, target.Status.Conversion) + require.True(t, target.Status.Conversion.Failed) + require.NotNil(t, target.Status.Conversion.Error) + require.Equal(t, dashv1.VERSION, *target.Status.Conversion.StoredVersion) + } + }) + + t.Run("Convert_V1beta1_to_V2beta1 success path returns nil", func(t *testing.T) { + source := &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": "test dashboard", + "schemaVersion": 25, // Valid schema version + }, + }, + } + target := &dashv2beta1.Dashboard{} + + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + + // Should succeed if dashboard is valid + if err == nil { + // Success path covered + require.NoError(t, err) + } + }) +} diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go new file mode 100644 index 00000000000..d47fbbc5ec1 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -0,0 +1,2414 @@ +package conversion + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apiserver/pkg/endpoints/request" + + "github.com/grafana/authlib/types" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + schemaversion "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +// getDefaultDatasourceType gets the default datasource type using the datasource provider +func getDefaultDatasourceType(ctx context.Context, provider schemaversion.DataSourceInfoProvider) string { + const defaultType = "grafana" + + if provider == nil { + return defaultType + } + + datasources := provider.GetDataSourceInfo(ctx) + for _, ds := range datasources { + if ds.Default { + return ds.Type + } + } + + return defaultType +} + +// getDatasourceTypeByUID gets the datasource type by UID using the datasource provider +func getDatasourceTypeByUID(ctx context.Context, uid string, provider schemaversion.DataSourceInfoProvider) string { + if uid == "" { + return getDefaultDatasourceType(ctx, provider) + } + + if provider == nil { + return "grafana" + } + + datasources := provider.GetDataSourceInfo(ctx) + for _, ds := range datasources { + if ds.UID == uid { + return ds.Type + } + } + + return getDefaultDatasourceType(ctx, provider) +} + +// prepareV1beta1ConversionContext sets up the context with namespace and service identity +// for v1beta1 dashboard conversions. This context is needed to retrieve datasources for +// converting dashboard datasource references. +// A background service identity is used because the user who is reading the specific dashboard +// may not have access to all the datasources in the dashboard, but the conversion still needs to take place +// in order to be able to convert between k8s versions. +func prepareV1beta1ConversionContext(in *dashv1.Dashboard, dsInfoProvider schemaversion.DataSourceInfoProvider) (context.Context, *types.NamespaceInfo, error) { + if dsInfoProvider == nil { + return nil, nil, fmt.Errorf("datasource provider not initialized") + } + + namespace := in.GetNamespace() + if namespace == "" { + return context.Background(), nil, nil + } + + // Try to parse namespace and set up tenant-aware context + ctx := request.WithNamespace(context.Background(), namespace) + nsInfo, err := types.ParseNamespace(namespace) + if err != nil { + return ctx, nil, nil + } + + ctx, _ = identity.WithServiceIdentity(ctx, nsInfo.OrgID) + + return ctx, &nsInfo, nil +} + +func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsInfoProvider schemaversion.DataSourceInfoProvider) error { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2alpha1.APIVERSION + out.Kind = in.Kind + + // Prepare context with namespace and service identity + // The datasource provider is passed as a parameter (captured in closure when conversions are registered) + ctx, _, err := prepareV1beta1ConversionContext(in, dsInfoProvider) + if err != nil { + // If context preparation fails, return error to be handled by wrapper + // The wrapper will set status and handle gracefully + return fmt.Errorf("failed to prepare conversion context: %w", err) + } + + return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsInfoProvider) +} + +func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsInfoProvider schemaversion.DataSourceInfoProvider) error { + // Parse the unstructured spec into a dashboard JSON structure + dashboardJSON, ok := in.Object["dashboard"] + if !ok { + // If no "dashboard" key, treat the entire object as the dashboard + dashboardJSON = in.Object + } + + // Convert to bytes and back to handle complex nested structures + dashBytes, err := json.Marshal(dashboardJSON) + if err != nil { + return fmt.Errorf("failed to marshal dashboard JSON: %w", err) + } + + var dashboard map[string]interface{} + if err := json.Unmarshal(dashBytes, &dashboard); err != nil { + return fmt.Errorf("failed to unmarshal dashboard JSON: %w", err) + } + + // Get defaults + timeSettingsDefaults := dashv2alpha1.NewDashboardTimeSettingsSpec() + dashboardDefaults := getDefaultDashboardV2Spec() + + // Transform basic fields + out.Title = schemaversion.GetStringValue(dashboard, "title") + out.Description = getStringPtr(dashboard, "description") + out.Tags = getStringSlice(dashboard, "tags") + out.CursorSync = transformCursorSyncToEnum(getIntField(dashboard, "graphTooltip", 0)) + out.Preload = getBoolField(dashboard, "preload", dashboardDefaults.Preload) + + // Add frontend-style default values + // Set default editable: true to match frontend behavior + defaultEditable := true + out.Editable = &defaultEditable + + // Set default liveNow: false to match frontend behavior + defaultLiveNow := false + out.LiveNow = &defaultLiveNow + + // Override with input values if they exist + if liveNow, exists := dashboard["liveNow"]; exists { + if liveNowBool, ok := liveNow.(bool); ok { + out.LiveNow = &liveNowBool + } + } + if editable, exists := dashboard["editable"]; exists { + if editableBool, ok := editable.(bool); ok { + out.Editable = &editableBool + } + } + if revision, exists := dashboard["revision"]; exists { + if revisionInt, ok := revision.(float64); ok { + revisionUint := uint16(revisionInt) + out.Revision = &revisionUint + } + } + + // Transform time settings + out.TimeSettings = transformTimeSettings(dashboard, timeSettingsDefaults) + + // Transform links + out.Links = transformLinks(dashboard) + + // Transform panels to elements and layout + elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsInfoProvider) + if err != nil { + return fmt.Errorf("failed to transform panels: %w", err) + } + out.Elements = elements + out.Layout = layout + + // Transform variables + variables, err := transformVariables(ctx, dashboard, dsInfoProvider) + if err != nil { + return fmt.Errorf("failed to transform variables: %w", err) + } + out.Variables = variables + + // Transform annotations + annotations, err := transformAnnotations(dashboard) + if err != nil { + return fmt.Errorf("failed to transform annotations: %w", err) + } + out.Annotations = annotations + + return nil +} + +func getStringPtr(m map[string]interface{}, key string) *string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok && str != "" { + return &str + } + } + return nil +} + +func getStringSlice(m map[string]interface{}, key string) []string { + if val, ok := m[key]; ok { + if slice, ok := val.([]interface{}); ok { + result := make([]string, 0, len(slice)) + for _, item := range slice { + if str, ok := item.(string); ok { + result = append(result, str) + } + } + return result + } + } + return []string{} +} + +func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool { + if val, ok := m[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return defaultValue +} + +// Helper function to create int64 pointer +func int64Ptr(i int64) *int64 { + return &i +} + +// isTemplateVariable checks if a string is a Grafana template variable reference +// Template variables can be in the form: $varname or ${varname} +func isTemplateVariable(s string) bool { + if s == "" { + return false + } + return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${") +} + +func getIntField(m map[string]interface{}, key string, defaultValue int) int { + if val, ok := m[key]; ok { + switch v := val.(type) { + case int: + return v + case float64: + return int(v) + case string: + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + } + return defaultValue +} + +// Enum transformation functions +func transformCursorSyncToEnum(cursorSync int) dashv2alpha1.DashboardDashboardCursorSync { + switch cursorSync { + case 0: + return dashv2alpha1.DashboardDashboardCursorSyncOff + case 1: + return dashv2alpha1.DashboardDashboardCursorSyncCrosshair + case 2: + return dashv2alpha1.DashboardDashboardCursorSyncTooltip + default: + return dashv2alpha1.DashboardDashboardCursorSyncOff + } +} + +func getDefaultDashboardV2Spec() dashv2alpha1.DashboardSpec { + return dashv2alpha1.DashboardSpec{ + Preload: false, + } +} + +// Transform time settings +func transformTimeSettings(dashboard map[string]interface{}, defaults *dashv2alpha1.DashboardTimeSettingsSpec) dashv2alpha1.DashboardTimeSettingsSpec { + timeSettings := dashv2alpha1.DashboardTimeSettingsSpec{ + From: defaults.From, + To: defaults.To, + Timezone: defaults.Timezone, + AutoRefresh: defaults.AutoRefresh, + AutoRefreshIntervals: defaults.AutoRefreshIntervals, + FiscalYearStartMonth: defaults.FiscalYearStartMonth, + HideTimepicker: defaults.HideTimepicker, + // Don't initialize optional fields with defaults - only set them if present in input + } + + // Extract time range + if timeRange, ok := dashboard["time"].(map[string]interface{}); ok { + if from := schemaversion.GetStringValue(timeRange, "from"); from != "" { + timeSettings.From = from + } + if to := schemaversion.GetStringValue(timeRange, "to"); to != "" { + timeSettings.To = to + } + } + + // Extract other time-related fields + if timezone, exists := dashboard["timezone"]; exists { + if timezoneStr, ok := timezone.(string); ok { + timeSettings.Timezone = &timezoneStr + } + } + if refresh := schemaversion.GetStringValue(dashboard, "refresh"); refresh != "" { + timeSettings.AutoRefresh = refresh + } + + timeSettings.FiscalYearStartMonth = int64(getIntField(dashboard, "fiscalYearStartMonth", int(defaults.FiscalYearStartMonth))) + + if weekStart, exists := dashboard["weekStart"]; exists { + if weekStartStr, ok := weekStart.(string); ok && weekStartStr != "" { + weekStartEnum := dashv2alpha1.DashboardTimeSettingsSpecWeekStart(weekStartStr) + timeSettings.WeekStart = &weekStartEnum + } + } + + // Extract timepicker settings + if timepicker, ok := dashboard["timepicker"].(map[string]interface{}); ok { + if intervals, ok := timepicker["refresh_intervals"].([]interface{}); ok { + refreshIntervals := make([]string, 0, len(intervals)) + for _, interval := range intervals { + if str, ok := interval.(string); ok { + refreshIntervals = append(refreshIntervals, str) + } + } + if len(refreshIntervals) > 0 { + timeSettings.AutoRefreshIntervals = refreshIntervals + } + } + timeSettings.HideTimepicker = getBoolField(timepicker, "hidden", defaults.HideTimepicker) + if nowDelay, exists := timepicker["nowDelay"]; exists { + if nowDelayStr, ok := nowDelay.(string); ok && nowDelayStr != "" { + timeSettings.NowDelay = &nowDelayStr + } + } + + // Handle quick_ranges + if quickRanges, ok := timepicker["quick_ranges"].([]interface{}); ok { + ranges := make([]dashv2alpha1.DashboardTimeRangeOption, 0, len(quickRanges)) + for _, qr := range quickRanges { + if qrMap, ok := qr.(map[string]interface{}); ok { + quickRange := dashv2alpha1.DashboardTimeRangeOption{ + Display: schemaversion.GetStringValue(qrMap, "display"), + From: schemaversion.GetStringValue(qrMap, "from"), + To: schemaversion.GetStringValue(qrMap, "to"), + } + ranges = append(ranges, quickRange) + } + } + timeSettings.QuickRanges = ranges + } + } + + return timeSettings +} + +// Transform links +func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDashboardLink { + if links, ok := dashboard["links"].([]interface{}); ok { + result := make([]dashv2alpha1.DashboardDashboardLink, 0, len(links)) + for _, link := range links { + if linkMap, ok := link.(map[string]interface{}); ok { + // Required fields with defaults + dashLink := dashv2alpha1.DashboardDashboardLink{ + Title: schemaversion.GetStringValue(linkMap, "title"), + Type: dashv2alpha1.DashboardDashboardLinkType(schemaversion.GetStringValue(linkMap, "type", "link")), + Icon: schemaversion.GetStringValue(linkMap, "icon"), + Tooltip: schemaversion.GetStringValue(linkMap, "tooltip"), + Tags: getStringSlice(linkMap, "tags"), + AsDropdown: getBoolField(linkMap, "asDropdown", false), + TargetBlank: getBoolField(linkMap, "targetBlank", false), + IncludeVars: getBoolField(linkMap, "includeVars", false), + KeepTime: getBoolField(linkMap, "keepTime", false), + } + + // Optional field - only set if present + if url, exists := linkMap["url"]; exists { + if urlStr, ok := url.(string); ok { + dashLink.Url = &urlStr + } + } + + result = append(result, dashLink) + } + } + return result + } + return []dashv2alpha1.DashboardDashboardLink{} +} + +// Panel transformation constants +const GRID_ROW_HEIGHT = 1 + +func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsInfoProvider schemaversion.DataSourceInfoProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { + panels, ok := dashboard["panels"].([]interface{}) + if !ok { + // Return empty elements and default grid layout + elements := make(map[string]dashv2alpha1.DashboardElement) + layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: []dashv2alpha1.DashboardGridLayoutItemKind{}, + }, + }, + } + return elements, layout, nil + } + + // Check if any panels are row panels + hasRowPanels := false + for _, p := range panels { + if panelMap, ok := p.(map[string]interface{}); ok { + if schemaversion.GetStringValue(panelMap, "type") == "row" { + hasRowPanels = true + break + } + } + } + + if hasRowPanels { + return convertToRowsLayout(ctx, panels, dsInfoProvider) + } + + return convertToGridLayout(ctx, panels, dsInfoProvider) +} + +func convertToGridLayout(ctx context.Context, panels []interface{}, dsInfoProvider schemaversion.DataSourceInfoProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { + elements := make(map[string]dashv2alpha1.DashboardElement) + items := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0, len(panels)) + + for _, p := range panels { + panelMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + + element, elementName, err := buildElement(ctx, panelMap, dsInfoProvider) + if err != nil { + continue // Skip invalid panels + } + + elements[elementName] = element + items = append(items, buildGridItemKind(panelMap, elementName, nil)) + } + + layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: items, + }, + }, + } + + return elements, layout, nil +} + +func convertToRowsLayout(ctx context.Context, panels []interface{}, dsInfoProvider schemaversion.DataSourceInfoProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { + elements := make(map[string]dashv2alpha1.DashboardElement) + rows := make([]dashv2alpha1.DashboardRowsLayoutRowKind, 0) + + var currentRow *dashv2alpha1.DashboardRowsLayoutRowKind + var legacyRowY int64 + + for _, p := range panels { + panelMap, ok := p.(map[string]interface{}) + if !ok { + continue + } + + if schemaversion.GetStringValue(panelMap, "type") == "row" { + // This is a row panel + if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok { + legacyRowY = int64(getIntField(gridPos, "y", 0)) + } + + if currentRow != nil { + // If currentRow is a hidden-header row (panels before first explicit row), + // set its collapse to match the first explicit row's collapsed value + // This matches frontend behavior: collapse: panel.collapsed + if currentRow.Spec.HideHeader != nil && *currentRow.Spec.HideHeader { + rowCollapsed := getBoolField(panelMap, "collapsed", false) + currentRow.Spec.Collapse = &rowCollapsed + } + // Flush current row to layout + rows = append(rows, *currentRow) + } + + // Handle collapsed row panels + rowElements := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0) + if collapsedPanels, ok := panelMap["panels"].([]interface{}); ok { + for _, panel := range collapsedPanels { + if collapsedPanelMap, ok := panel.(map[string]interface{}); ok { + element, name, err := buildElement(ctx, collapsedPanelMap, dsInfoProvider) + if err == nil { + elements[name] = element + rowElements = append(rowElements, buildGridItemKind(collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY)))) + } + } + } + } + + currentRow = buildRowKind(panelMap, rowElements) + } else { + // Regular panel + element, elementName, err := buildElement(ctx, panelMap, dsInfoProvider) + if err != nil { + continue // Skip invalid panels + } + + elements[elementName] = element + + if currentRow != nil { + // Add to current row + if currentRow.Spec.Layout.GridLayoutKind != nil { + currentRow.Spec.Layout.GridLayoutKind.Spec.Items = append( + currentRow.Spec.Layout.GridLayoutKind.Spec.Items, + buildGridItemKind(panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY))), + ) + } + } else { + // Create first row (hidden header) + // Since this row does not exist in V1, we simulate it being outside of the grid above the first panel + // The Y position does not matter for the rows layout, but it's used to calculate the position of the panels in the grid layout in the row. + legacyRowY = -1 + gridItems := []dashv2alpha1.DashboardGridLayoutItemKind{ + buildGridItemKind(panelMap, elementName, int64Ptr(0)), + } + + hideHeader := true + currentRow = &dashv2alpha1.DashboardRowsLayoutRowKind{ + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Collapse: &[]bool{false}[0], + Title: &[]string{""}[0], + HideHeader: &hideHeader, + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: gridItems, + }, + }, + }, + }, + } + } + } + } + + if currentRow != nil { + // Flush last row + rows = append(rows, *currentRow) + } + + layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + RowsLayoutKind: &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: dashv2alpha1.DashboardRowsLayoutSpec{ + Rows: rows, + }, + }, + } + + return elements, layout, nil +} + +func buildElement(ctx context.Context, panelMap map[string]interface{}, dsInfoProvider schemaversion.DataSourceInfoProvider) (dashv2alpha1.DashboardElement, string, error) { + panelID := getIntField(panelMap, "id", 0) + elementName := fmt.Sprintf("panel-%d", panelID) + + // Check if it's a library panel + if libraryPanel, ok := panelMap["libraryPanel"].(map[string]interface{}); ok { + libPanelKind := &dashv2alpha1.DashboardLibraryPanelKind{ + Kind: "LibraryPanel", + Spec: dashv2alpha1.DashboardLibraryPanelKindSpec{ + LibraryPanel: dashv2alpha1.DashboardLibraryPanelRef{ + Uid: schemaversion.GetStringValue(libraryPanel, "uid"), + Name: schemaversion.GetStringValue(libraryPanel, "name"), + }, + Id: float64(panelID), + Title: schemaversion.GetStringValue(panelMap, "title"), + }, + } + + element := dashv2alpha1.DashboardElement{ + LibraryPanelKind: libPanelKind, + } + + return element, elementName, nil + } + + // Regular panel + panelKind, err := buildPanelKind(ctx, panelMap, dsInfoProvider) + if err != nil { + return dashv2alpha1.DashboardElement{}, "", err + } + + element := dashv2alpha1.DashboardElement{ + PanelKind: panelKind, + } + + return element, elementName, nil +} + +func buildPanelKind(ctx context.Context, panelMap map[string]interface{}, dsInfoProvider schemaversion.DataSourceInfoProvider) (*dashv2alpha1.DashboardPanelKind, error) { + panelID := float64(getIntField(panelMap, "id", 0)) + + // Transform queries + queries := transformPanelQueries(ctx, panelMap, dsInfoProvider) + + // Transform transformations + transformations := transformPanelTransformations(panelMap) + + // Build query options + queryOptions := buildQueryOptions(panelMap) + + // Build data links + links := transformDataLinks(panelMap) + + // Build viz config + vizConfig := buildVizConfig(panelMap) + + panelKind := &dashv2alpha1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2alpha1.DashboardPanelSpec{ + Id: panelID, + Title: schemaversion.GetStringValue(panelMap, "title"), + Description: schemaversion.GetStringValue(panelMap, "description"), + Links: links, + Data: dashv2alpha1.DashboardQueryGroupKind{ + Kind: "QueryGroup", + Spec: dashv2alpha1.DashboardQueryGroupSpec{ + Queries: queries, + Transformations: transformations, + QueryOptions: queryOptions, + }, + }, + VizConfig: vizConfig, + }, + } + + // Handle transparent panels + if transparent := getBoolField(panelMap, "transparent", false); transparent { + panelKind.Spec.Transparent = &transparent + } + + return panelKind, nil +} + +func buildGridItemKind(panelMap map[string]interface{}, elementName string, yOverride *int64) dashv2alpha1.DashboardGridLayoutItemKind { + // Default grid position (matches frontend PanelModel defaults: w=6, h=3) + x, y, width, height := int64(0), int64(0), int64(6), int64(3) + + if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok { + x = int64(getIntField(gridPos, "x", 0)) + y = int64(getIntField(gridPos, "y", 0)) + width = int64(getIntField(gridPos, "w", 6)) + height = int64(getIntField(gridPos, "h", 3)) + } + + // Apply frontend-style grid position calculations + // Frontend recalculates positions based on row structure + if yOverride != nil { + y = *yOverride + } + + item := dashv2alpha1.DashboardGridLayoutItemKind{ + Kind: "GridLayoutItem", + Spec: dashv2alpha1.DashboardGridLayoutItemSpec{ + X: x, + Y: y, + Width: width, + Height: height, + Element: dashv2alpha1.DashboardElementReference{ + Kind: "ElementReference", + Name: elementName, + }, + }, + } + + // Handle repeat options + if repeat := schemaversion.GetStringValue(panelMap, "repeat"); repeat != "" { + repeatOptions := &dashv2alpha1.DashboardRepeatOptions{ + Mode: "variable", + Value: repeat, + } + + if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" { + switch repeatDirection { + case "h": + direction := dashv2alpha1.DashboardRepeatOptionsDirectionH + repeatOptions.Direction = &direction + case "v": + direction := dashv2alpha1.DashboardRepeatOptionsDirectionV + repeatOptions.Direction = &direction + } + } + + if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 { + maxPerRowInt64 := int64(maxPerRow) + repeatOptions.MaxPerRow = &maxPerRowInt64 + } + + item.Spec.Repeat = repeatOptions + } + + return item +} + +func buildRowKind(rowPanelMap map[string]interface{}, elements []dashv2alpha1.DashboardGridLayoutItemKind) *dashv2alpha1.DashboardRowsLayoutRowKind { + collapsed := getBoolField(rowPanelMap, "collapsed", false) + title := schemaversion.GetStringValue(rowPanelMap, "title") + + row := &dashv2alpha1.DashboardRowsLayoutRowKind{ + Kind: "RowsLayoutRow", + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Collapse: &collapsed, + Title: &title, + Layout: dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{ + Items: elements, + }, + }, + }, + }, + } + + // Handle repeat options for rows + if repeat := schemaversion.GetStringValue(rowPanelMap, "repeat"); repeat != "" { + row.Spec.Repeat = &dashv2alpha1.DashboardRowRepeatOptions{ + Mode: "variable", + Value: repeat, + } + } + + return row +} + +func yOffsetInRows(panelMap map[string]interface{}, rowY int64) int64 { + if gridPos, ok := panelMap["gridPos"].(map[string]interface{}); ok { + panelY := int64(getIntField(gridPos, "y", 0)) + return panelY - rowY - GRID_ROW_HEIGHT + } + return 0 +} + +// LEGACY_STRING_VALUE_KEY is used for QueryVariableKind's query prop. Historically, queries +// could be defined as strings, but in schema V2 we've deprecated the string type and support +// only DataQuery objects. When a query is a simple string (legacy format), it's stored under +// this key in a map structure to preserve the value during migration from v1beta1 to v2alpha1. +const LEGACY_STRING_VALUE_KEY = "__legacyStringValue" + +// Variable enum transformation functions +func transformVariableHideToEnum(hide interface{}) dashv2alpha1.DashboardVariableHide { + switch v := hide.(type) { + case int: + switch v { + case 0: + return dashv2alpha1.DashboardVariableHideDontHide + case 1: + return dashv2alpha1.DashboardVariableHideHideLabel + case 2: + return dashv2alpha1.DashboardVariableHideHideVariable + default: + return dashv2alpha1.DashboardVariableHideDontHide + } + case float64: + return transformVariableHideToEnum(int(v)) + case string: + switch v { + case "dontHide", "": + return dashv2alpha1.DashboardVariableHideDontHide + case "hideLabel": + return dashv2alpha1.DashboardVariableHideHideLabel + case "hideVariable": + return dashv2alpha1.DashboardVariableHideHideVariable + default: + return dashv2alpha1.DashboardVariableHideDontHide + } + default: + return dashv2alpha1.DashboardVariableHideDontHide + } +} + +func transformVariableRefreshToEnum(refresh interface{}) dashv2alpha1.DashboardVariableRefresh { + switch v := refresh.(type) { + case int: + switch v { + case 0: + return dashv2alpha1.DashboardVariableRefreshNever + case 1: + return dashv2alpha1.DashboardVariableRefreshOnDashboardLoad + case 2: + return dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged + default: + return dashv2alpha1.DashboardVariableRefreshNever + } + case float64: + return transformVariableRefreshToEnum(int(v)) + case string: + switch v { + case "never", "": + return dashv2alpha1.DashboardVariableRefreshNever + case "onDashboardLoad": + return dashv2alpha1.DashboardVariableRefreshOnDashboardLoad + case "onTimeRangeChanged": + return dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged + default: + return dashv2alpha1.DashboardVariableRefreshNever + } + default: + return dashv2alpha1.DashboardVariableRefreshNever + } +} + +func transformVariableSortToEnum(sort interface{}) dashv2alpha1.DashboardVariableSort { + switch v := sort.(type) { + case int: + switch v { + case 0: + return dashv2alpha1.DashboardVariableSortDisabled + case 1: + return dashv2alpha1.DashboardVariableSortAlphabeticalAsc + case 2: + return dashv2alpha1.DashboardVariableSortAlphabeticalDesc + case 3: + return dashv2alpha1.DashboardVariableSortNumericalAsc + case 4: + return dashv2alpha1.DashboardVariableSortNumericalDesc + default: + return dashv2alpha1.DashboardVariableSortDisabled + } + case float64: + return transformVariableSortToEnum(int(v)) + case string: + switch v { + case "disabled", "": + return dashv2alpha1.DashboardVariableSortDisabled + case "alphabeticalAsc": + return dashv2alpha1.DashboardVariableSortAlphabeticalAsc + case "alphabeticalDesc": + return dashv2alpha1.DashboardVariableSortAlphabeticalDesc + case "numericalAsc": + return dashv2alpha1.DashboardVariableSortNumericalAsc + case "numericalDesc": + return dashv2alpha1.DashboardVariableSortNumericalDesc + case "alphabeticalCaseInsensitiveAsc": + return dashv2alpha1.DashboardVariableSortAlphabeticalCaseInsensitiveAsc + case "alphabeticalCaseInsensitiveDesc": + return dashv2alpha1.DashboardVariableSortAlphabeticalCaseInsensitiveDesc + case "naturalAsc": + return dashv2alpha1.DashboardVariableSortNaturalAsc + case "naturalDesc": + return dashv2alpha1.DashboardVariableSortNaturalDesc + default: + return dashv2alpha1.DashboardVariableSortDisabled + } + default: + return dashv2alpha1.DashboardVariableSortDisabled + } +} + +func transformVariables(ctx context.Context, dashboard map[string]interface{}, dsInfoProvider schemaversion.DataSourceInfoProvider) ([]dashv2alpha1.DashboardVariableKind, error) { + templating, ok := dashboard["templating"].(map[string]interface{}) + if !ok { + return []dashv2alpha1.DashboardVariableKind{}, nil + } + + list, ok := templating["list"].([]interface{}) + if !ok { + return []dashv2alpha1.DashboardVariableKind{}, nil + } + + variables := make([]dashv2alpha1.DashboardVariableKind, 0, len(list)) + + for _, v := range list { + varMap, ok := v.(map[string]interface{}) + if !ok { + continue + } + + // Extract common properties + commonProps := extractCommonVariableProperties(varMap) + varType := schemaversion.GetStringValue(varMap, "type") + + switch varType { + case "query": + if queryVar, err := buildQueryVariable(ctx, varMap, commonProps, dsInfoProvider); err == nil { + variables = append(variables, queryVar) + } + case "datasource": + if dsVar, err := buildDatasourceVariable(ctx, varMap, commonProps, dsInfoProvider); err == nil { + variables = append(variables, dsVar) + } + case "custom": + if customVar, err := buildCustomVariable(varMap, commonProps); err == nil { + variables = append(variables, customVar) + } + case "adhoc": + if adhocVar, err := buildAdhocVariable(ctx, varMap, commonProps, dsInfoProvider); err == nil { + variables = append(variables, adhocVar) + } + case "constant": + if constantVar, err := buildConstantVariable(varMap, commonProps); err == nil { + variables = append(variables, constantVar) + } + case "interval": + if intervalVar, err := buildIntervalVariable(varMap, commonProps); err == nil { + variables = append(variables, intervalVar) + } + case "textbox": + if textVar, err := buildTextVariable(varMap, commonProps); err == nil { + variables = append(variables, textVar) + } + case "groupby": + if groupByVar, err := buildGroupByVariable(ctx, varMap, commonProps, dsInfoProvider); err == nil { + variables = append(variables, groupByVar) + } + default: + // Skip unknown variable types + continue + } + } + + return variables, nil +} + +// Common variable properties extraction +type CommonVariableProperties struct { + Name string + Label *string + Description *string + Hide dashv2alpha1.DashboardVariableHide + SkipUrlSync bool +} + +func extractCommonVariableProperties(varMap map[string]interface{}) CommonVariableProperties { + props := CommonVariableProperties{ + Name: schemaversion.GetStringValue(varMap, "name"), + Hide: transformVariableHideToEnum(varMap["hide"]), + SkipUrlSync: getBoolField(varMap, "skipUrlSync", false), + } + + if label := schemaversion.GetStringValue(varMap, "label"); label != "" { + props.Label = &label + } + + if description := schemaversion.GetStringValue(varMap, "description"); description != "" { + props.Description = &description + } + + return props +} + +// Helper function to build variable options +func buildVariableOptions(options interface{}) []dashv2alpha1.DashboardVariableOption { + if options == nil { + return []dashv2alpha1.DashboardVariableOption{} + } + + optionsSlice, ok := options.([]interface{}) + if !ok { + return []dashv2alpha1.DashboardVariableOption{} + } + + result := make([]dashv2alpha1.DashboardVariableOption, 0, len(optionsSlice)) + for _, opt := range optionsSlice { + if optMap, ok := opt.(map[string]interface{}); ok { + selected := getBoolField(optMap, "selected", false) + option := dashv2alpha1.DashboardVariableOption{ + Text: buildStringOrArrayOfString(optMap["text"]), + Value: buildStringOrArrayOfString(optMap["value"]), + Selected: &selected, + } + result = append(result, option) + } + } + return result +} + +// Helper function to build DashboardStringOrArrayOfString +func buildStringOrArrayOfString(value interface{}) dashv2alpha1.DashboardStringOrArrayOfString { + switch v := value.(type) { + case string: + return dashv2alpha1.DashboardStringOrArrayOfString{ + String: &v, + } + case []interface{}: + stringArray := make([]string, 0, len(v)) + for _, item := range v { + if str, ok := item.(string); ok { + stringArray = append(stringArray, str) + } + } + return dashv2alpha1.DashboardStringOrArrayOfString{ + ArrayOfString: stringArray, + } + case map[string]interface{}: + // Handle legacy string value format + if legacyValue, ok := v[LEGACY_STRING_VALUE_KEY].(string); ok { + return dashv2alpha1.DashboardStringOrArrayOfString{ + String: &legacyValue, + } + } + // Fallback for other map types + empty := "" + return dashv2alpha1.DashboardStringOrArrayOfString{ + String: &empty, + } + default: + empty := "" + return dashv2alpha1.DashboardStringOrArrayOfString{ + String: &empty, + } + } +} + +// Helper function to build variable current value +func buildVariableCurrent(current interface{}) dashv2alpha1.DashboardVariableOption { + if current == nil { + empty := "" + return dashv2alpha1.DashboardVariableOption{ + Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty}, + Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty}, + } + } + + if currentMap, ok := current.(map[string]interface{}); ok { + return dashv2alpha1.DashboardVariableOption{ + Text: buildStringOrArrayOfString(currentMap["text"]), + Value: buildStringOrArrayOfString(currentMap["value"]), + } + } + + // Fallback for simple values + empty := "" + return dashv2alpha1.DashboardVariableOption{ + Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty}, + Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &empty}, + } +} + +// Helper function to build DataQuery kind +func buildDataQueryKind(query interface{}, datasourceType string) dashv2alpha1.DashboardDataQueryKind { + var querySpec map[string]interface{} + + switch q := query.(type) { + case string: + // Only include LEGACY_STRING_VALUE_KEY if query is a non-empty string + querySpec = make(map[string]interface{}) + if q != "" { + querySpec[LEGACY_STRING_VALUE_KEY] = q + } + case map[string]interface{}: + // Use query directly to match frontend behavior + querySpec = q + default: + querySpec = make(map[string]interface{}) + } + + return dashv2alpha1.DashboardDataQueryKind{ + Kind: "DataQuery", + Spec: querySpec, + } +} + +// Helper function to build DataQuery kind for variables (filters out refId) +func buildDataQueryKindForVariable(query interface{}, datasourceType string) dashv2alpha1.DashboardDataQueryKind { + var querySpec map[string]interface{} + + switch q := query.(type) { + case string: + // Only include LEGACY_STRING_VALUE_KEY if query is a non-empty string + querySpec = make(map[string]interface{}) + if q != "" { + querySpec[LEGACY_STRING_VALUE_KEY] = q + } + case map[string]interface{}: + // Filter out refId for variables (refId should only be in panel queries) + querySpec = q + default: + querySpec = make(map[string]interface{}) + } + + return dashv2alpha1.DashboardDataQueryKind{ + Kind: "DataQuery", + Spec: querySpec, + } +} + +// Query Variable +func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsInfoProvider schemaversion.DataSourceInfoProvider) (dashv2alpha1.DashboardVariableKind, error) { + datasource := varMap["datasource"] + var datasourceType, datasourceUID string + + if ds, ok := datasource.(map[string]interface{}); ok { + datasourceUID = schemaversion.GetStringValue(ds, "uid") + datasourceType = schemaversion.GetStringValue(ds, "type") + + // If we have a UID, use it to get the correct type from the datasource service + // BUT: Don't try to resolve types for template variables + if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) { + datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsInfoProvider) + } else if datasourceUID == "" && datasourceType == "" { + // If no UID and no type, use default + datasourceType = getDefaultDatasourceType(ctx, dsInfoProvider) + } + } else { + datasourceType = getDefaultDatasourceType(ctx, dsInfoProvider) + } + + queryVar := &dashv2alpha1.DashboardQueryVariableKind{ + Kind: "QueryVariable", + Spec: dashv2alpha1.DashboardQueryVariableSpec{ + Name: commonProps.Name, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + Current: buildVariableCurrent(varMap["current"]), + Multi: getBoolField(varMap, "multi", false), + IncludeAll: getBoolField(varMap, "includeAll", false), + Refresh: transformVariableRefreshToEnum(varMap["refresh"]), + Sort: transformVariableSortToEnum(varMap["sort"]), + Regex: schemaversion.GetStringValue(varMap, "regex"), + Query: buildDataQueryKindForVariable(varMap["query"], datasourceType), + AllowCustomValue: getBoolField(varMap, "allowCustomValue", true), + }, + } + + // Only set definition if it exists in the input (match frontend behavior) + if def, exists := varMap["definition"]; exists && def != nil { + if defStr, ok := def.(string); ok { + queryVar.Spec.Definition = &defStr + } + } + + // Only include datasource if datasourceUID exists (matching frontend behavior) + if datasourceUID != "" { + dsRef := &dashv2alpha1.DashboardDataSourceRef{ + Type: &datasourceType, + Uid: &datasourceUID, + } + queryVar.Spec.Datasource = dsRef + } + + // Always set options (matching frontend behavior) + queryVar.Spec.Options = buildVariableOptions(varMap["options"]) + + if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" { + queryVar.Spec.AllValue = &allValue + } + + return dashv2alpha1.DashboardVariableKind{ + QueryVariableKind: queryVar, + }, nil +} + +// Datasource Variable +func buildDatasourceVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsInfoProvider schemaversion.DataSourceInfoProvider) (dashv2alpha1.DashboardVariableKind, error) { + pluginId := getDefaultDatasourceType(ctx, dsInfoProvider) + if query := varMap["query"]; query != nil { + if queryStr, ok := query.(string); ok { + pluginId = queryStr + } + } + + dsVar := &dashv2alpha1.DashboardDatasourceVariableKind{ + Kind: "DatasourceVariable", + Spec: dashv2alpha1.DashboardDatasourceVariableSpec{ + Name: commonProps.Name, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + PluginId: pluginId, + Current: buildVariableCurrent(varMap["current"]), + Multi: getBoolField(varMap, "multi", false), + IncludeAll: getBoolField(varMap, "includeAll", false), + Refresh: transformVariableRefreshToEnum(varMap["refresh"]), + Regex: schemaversion.GetStringValue(varMap, "regex"), + AllowCustomValue: getBoolField(varMap, "allowCustomValue", true), + }, + } + + // Always set options (matching frontend behavior) + dsVar.Spec.Options = buildVariableOptions(varMap["options"]) + + if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" { + dsVar.Spec.AllValue = &allValue + } + + return dashv2alpha1.DashboardVariableKind{ + DatasourceVariableKind: dsVar, + }, nil +} + +// Custom Variable +func buildCustomVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) { + customVar := &dashv2alpha1.DashboardCustomVariableKind{ + Kind: "CustomVariable", + Spec: dashv2alpha1.DashboardCustomVariableSpec{ + Name: commonProps.Name, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + Query: schemaversion.GetStringValue(varMap, "query"), + Current: buildVariableCurrent(varMap["current"]), + Multi: getBoolField(varMap, "multi", false), + IncludeAll: getBoolField(varMap, "includeAll", false), + AllowCustomValue: getBoolField(varMap, "allowCustomValue", true), + }, + } + + // Always set options (matching frontend behavior) + customVar.Spec.Options = buildVariableOptions(varMap["options"]) + + if allValue := schemaversion.GetStringValue(varMap, "allValue"); allValue != "" { + customVar.Spec.AllValue = &allValue + } + + return dashv2alpha1.DashboardVariableKind{ + CustomVariableKind: customVar, + }, nil +} + +// Constant Variable +func buildConstantVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) { + // Get query value - if missing, it should be undefined (not empty string) + // Frontend uses variable.query for value, so if query is missing, value is undefined + var queryValue interface{} + if query, exists := varMap["query"]; exists { + if queryStr, ok := query.(string); ok { + queryValue = queryStr + } else { + queryValue = query + } + } + + // Build current from query value (matching frontend: value: variable.query, text: String(variable.state.value)) + // If query is undefined, frontend produces: text: "undefined", value: undefined (removed), query: undefined (removed) + var current dashv2alpha1.DashboardVariableOption + if currentVal, exists := varMap["current"]; exists && currentVal != nil { + // Use provided current if it exists + current = buildVariableCurrent(currentVal) + } else if queryValue != nil { + // Build current from query value (matching frontend behavior) + queryStr := "" + if str, ok := queryValue.(string); ok { + queryStr = str + } + current = dashv2alpha1.DashboardVariableOption{ + Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &queryStr}, + Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &queryStr}, + } + } else { + // query is undefined - match frontend: value ? String(value) : '' (defaults to empty string, not "undefined") + emptyStr := "" + current = dashv2alpha1.DashboardVariableOption{ + Text: dashv2alpha1.DashboardStringOrArrayOfString{String: &emptyStr}, + Value: dashv2alpha1.DashboardStringOrArrayOfString{String: &emptyStr}, + } + } + + // Get query value - frontend sets query: variable.state.value which can be undefined + // Since Query is a required string field, we must set it, but when query is missing from v1, + // we set it to empty string (frontend would have undefined which gets removed by sortedDeepCloneWithoutNulls) + var query string + if queryValue != nil { + if queryStr, ok := queryValue.(string); ok { + query = queryStr + } + } + // If queryValue is nil, query remains empty string "" + + constantVar := &dashv2alpha1.DashboardConstantVariableKind{ + Kind: "ConstantVariable", + Spec: dashv2alpha1.DashboardConstantVariableSpec{ + Name: commonProps.Name, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + Query: query, + Current: current, + }, + } + + return dashv2alpha1.DashboardVariableKind{ + ConstantVariableKind: constantVar, + }, nil +} + +// Interval Variable +func buildIntervalVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) { + query := schemaversion.GetStringValue(varMap, "query") + + // If query is missing/empty but options exist, generate query from options (matches frontend behavior) + if query == "" { + if options, ok := varMap["options"].([]interface{}); ok && len(options) > 0 { + queryValues := make([]string, 0, len(options)) + for _, opt := range options { + if optMap, ok := opt.(map[string]interface{}); ok { + if value, exists := optMap["value"]; exists { + if valueStr, ok := value.(string); ok && valueStr != "" { + queryValues = append(queryValues, valueStr) + } + } else if text, exists := optMap["text"]; exists { + if textStr, ok := text.(string); ok && textStr != "" { + queryValues = append(queryValues, textStr) + } + } + } + } + if len(queryValues) > 0 { + // Join interval values with comma, matching frontend getIntervalsQueryFromNewIntervalModel + query = strings.Join(queryValues, ",") + } + } + } + + intervalVar := &dashv2alpha1.DashboardIntervalVariableKind{ + Kind: "IntervalVariable", + Spec: dashv2alpha1.DashboardIntervalVariableSpec{ + Name: commonProps.Name, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + Query: query, + Current: buildVariableCurrent(varMap["current"]), + Refresh: dashv2alpha1.DashboardVariableRefreshOnTimeRangeChanged, // Always onTimeRangeChanged for interval + Auto: getBoolField(varMap, "auto", false), + AutoMin: schemaversion.GetStringValue(varMap, "auto_min"), + AutoCount: int64(getIntField(varMap, "auto_count", 0)), + }, + } + + // Always set options (matching frontend behavior) + intervalVar.Spec.Options = buildVariableOptions(varMap["options"]) + + return dashv2alpha1.DashboardVariableKind{ + IntervalVariableKind: intervalVar, + }, nil +} + +// Text Variable +func buildTextVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) { + textVar := &dashv2alpha1.DashboardTextVariableKind{ + Kind: "TextVariable", + Spec: dashv2alpha1.DashboardTextVariableSpec{ + Name: commonProps.Name, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + Query: schemaversion.GetStringValue(varMap, "query"), + Current: buildVariableCurrent(varMap["current"]), + }, + } + + return dashv2alpha1.DashboardVariableKind{ + TextVariableKind: textVar, + }, nil +} + +// Adhoc Variable +func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsInfoProvider schemaversion.DataSourceInfoProvider) (dashv2alpha1.DashboardVariableKind, error) { + datasource := varMap["datasource"] + var datasourceType, datasourceUID string + + if ds, ok := datasource.(map[string]interface{}); ok { + datasourceUID = schemaversion.GetStringValue(ds, "uid") + datasourceType = schemaversion.GetStringValue(ds, "type") + + // If we have a UID, use it to get the correct type from the datasource service + // BUT: Don't try to resolve types for template variables + if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) { + datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsInfoProvider) + } else if datasourceUID == "" && datasourceType == "" { + // If no UID and no type, use default + datasourceType = getDefaultDatasourceType(ctx, dsInfoProvider) + } + } else { + datasourceType = getDefaultDatasourceType(ctx, dsInfoProvider) + } + + adhocVar := &dashv2alpha1.DashboardAdhocVariableKind{ + Kind: "AdhocVariable", + Spec: dashv2alpha1.DashboardAdhocVariableSpec{ + Name: commonProps.Name, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + AllowCustomValue: getBoolField(varMap, "allowCustomValue", true), + }, + } + + // Transform baseFilters if they exist + if baseFilters, exists := varMap["baseFilters"]; exists { + if baseFiltersArray, ok := baseFilters.([]interface{}); ok { + adhocVar.Spec.BaseFilters = transformAdHocFilters(baseFiltersArray) + } + } + + // Transform filters if they exist + if filters, exists := varMap["filters"]; exists { + if filtersArray, ok := filters.([]interface{}); ok { + adhocVar.Spec.Filters = transformAdHocFilters(filtersArray) + } + } + + // Transform defaultKeys if they exist + if defaultKeys, exists := varMap["defaultKeys"]; exists { + if defaultKeysArray, ok := defaultKeys.([]interface{}); ok { + adhocVar.Spec.DefaultKeys = transformMetricFindValues(defaultKeysArray) + } + } + + // Only include datasource if datasourceUID exists (matching frontend behavior) + if datasourceUID != "" { + dsRef := &dashv2alpha1.DashboardDataSourceRef{ + Type: &datasourceType, + Uid: &datasourceUID, + } + adhocVar.Spec.Datasource = dsRef + } + + return dashv2alpha1.DashboardVariableKind{ + AdhocVariableKind: adhocVar, + }, nil +} + +// Helper function to transform adhoc filters +func transformAdHocFilters(filters []interface{}) []dashv2alpha1.DashboardAdHocFilterWithLabels { + result := make([]dashv2alpha1.DashboardAdHocFilterWithLabels, 0, len(filters)) + + for _, filter := range filters { + if filterMap, ok := filter.(map[string]interface{}); ok { + // Only include filters that don't have an origin or have origin "dashboard" + // This matches the frontend validateFiltersOrigin logic + if origin, exists := filterMap["origin"]; !exists || origin == "dashboard" { + adhocFilter := dashv2alpha1.DashboardAdHocFilterWithLabels{ + Key: schemaversion.GetStringValue(filterMap, "key"), + Operator: schemaversion.GetStringValue(filterMap, "operator", "="), + Value: schemaversion.GetStringValue(filterMap, "value"), + } + + // Handle optional fields + if keyLabel := schemaversion.GetStringValue(filterMap, "keyLabel"); keyLabel != "" { + adhocFilter.KeyLabel = &keyLabel + } + if condition := schemaversion.GetStringValue(filterMap, "condition"); condition != "" { + adhocFilter.Condition = &condition + } + + // Handle multi-value filters + if values, exists := filterMap["values"]; exists { + if valuesArray, ok := values.([]interface{}); ok { + stringValues := make([]string, 0, len(valuesArray)) + for _, v := range valuesArray { + if str, ok := v.(string); ok { + stringValues = append(stringValues, str) + } + } + if len(stringValues) > 0 { + adhocFilter.Values = stringValues + } + } + } + + // Handle value labels for multi-value filters + if valueLabels, exists := filterMap["valueLabels"]; exists { + if valueLabelsArray, ok := valueLabels.([]interface{}); ok { + stringValueLabels := make([]string, 0, len(valueLabelsArray)) + for _, v := range valueLabelsArray { + if str, ok := v.(string); ok { + stringValueLabels = append(stringValueLabels, str) + } + } + if len(stringValueLabels) > 0 { + adhocFilter.ValueLabels = stringValueLabels + } + } + } + + result = append(result, adhocFilter) + } + } + } + + return result +} + +// Helper function to transform metric find values +func transformMetricFindValues(values []interface{}) []dashv2alpha1.DashboardMetricFindValue { + result := make([]dashv2alpha1.DashboardMetricFindValue, 0, len(values)) + + for _, value := range values { + if valueMap, ok := value.(map[string]interface{}); ok { + // Convert object format to string format to match frontend behavior + text := schemaversion.GetStringValue(valueMap, "text") + if text != "" { + // Use text as the string value to match frontend behavior + metricFindValue := dashv2alpha1.DashboardMetricFindValue{ + Text: text, + Value: &dashv2alpha1.DashboardStringOrFloat64{ + String: &text, + }, + } + result = append(result, metricFindValue) + } + } else if str, ok := value.(string); ok { + // Handle simple string values - convert to object format with text and value + metricFindValue := dashv2alpha1.DashboardMetricFindValue{ + Text: str, + Value: &dashv2alpha1.DashboardStringOrFloat64{ + String: &str, + }, + } + result = append(result, metricFindValue) + } + } + + return result +} + +// GroupBy Variable +func buildGroupByVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsInfoProvider schemaversion.DataSourceInfoProvider) (dashv2alpha1.DashboardVariableKind, error) { + datasource := varMap["datasource"] + var datasourceType, datasourceUID string + + if ds, ok := datasource.(map[string]interface{}); ok { + datasourceUID = schemaversion.GetStringValue(ds, "uid") + datasourceType = schemaversion.GetStringValue(ds, "type") + + // If we have a UID, use it to get the correct type from the datasource service + // BUT: Don't try to resolve types for template variables + if datasourceUID != "" && datasourceType == "" && !isTemplateVariable(datasourceUID) { + datasourceType = getDatasourceTypeByUID(ctx, datasourceUID, dsInfoProvider) + } else if datasourceUID == "" && datasourceType == "" { + // If no UID and no type, use default + datasourceType = getDefaultDatasourceType(ctx, dsInfoProvider) + } + } else { + datasourceType = getDefaultDatasourceType(ctx, dsInfoProvider) + } + + groupByVar := &dashv2alpha1.DashboardGroupByVariableKind{ + Kind: "GroupByVariable", + Spec: dashv2alpha1.DashboardGroupByVariableSpec{ + Name: commonProps.Name, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + Current: buildVariableCurrent(varMap["current"]), + Multi: getBoolField(varMap, "multi", false), + }, + } + + // Only include datasource if datasourceUID exists + if datasourceUID != "" { + dsRef := &dashv2alpha1.DashboardDataSourceRef{ + Type: &datasourceType, + Uid: &datasourceUID, + } + groupByVar.Spec.Datasource = dsRef + } + + // Always set options (matching frontend behavior) + groupByVar.Spec.Options = buildVariableOptions(varMap["options"]) + + return dashv2alpha1.DashboardVariableKind{ + GroupByVariableKind: groupByVar, + }, nil +} + +func transformAnnotations(dashboard map[string]interface{}) ([]dashv2alpha1.DashboardAnnotationQueryKind, error) { + annotations, ok := dashboard["annotations"].(map[string]interface{}) + if !ok { + // Return empty array to match frontend behavior + return []dashv2alpha1.DashboardAnnotationQueryKind{}, nil + } + + list, ok := annotations["list"].([]interface{}) + if !ok || len(list) == 0 { + // Return empty array to match frontend behavior + return []dashv2alpha1.DashboardAnnotationQueryKind{}, nil + } + + result := make([]dashv2alpha1.DashboardAnnotationQueryKind, 0, len(list)) + + for _, a := range list { + annotationMap, ok := a.(map[string]interface{}) + if !ok { + continue + } + + if annotationQuery, err := buildAnnotationQuery(annotationMap); err == nil { + result = append(result, annotationQuery) + } + } + + return result, nil +} + +func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.DashboardAnnotationQueryKind, error) { + // Extract datasource information + var datasourceRef *dashv2alpha1.DashboardDataSourceRef + var datasourceType, datasourceUID string + + if datasource, ok := annotationMap["datasource"].(map[string]interface{}); ok { + datasourceUID = schemaversion.GetStringValue(datasource, "uid") + datasourceType = schemaversion.GetStringValue(datasource, "type") + + // Only create datasource ref if the annotation actually has a valid datasource type in the original input + // This matches frontend behavior: if (dsUID && annotation.datasource && annotation.datasource.type) + // We don't try to resolve datasources that are missing a type - they should be resolved at runtime + if datasourceUID != "" && datasourceType != "" { + datasourceRef = &dashv2alpha1.DashboardDataSourceRef{ + Type: &datasourceType, + Uid: &datasourceUID, + } + } + } + + // Build the query from target + var query *dashv2alpha1.DashboardDataQueryKind + if target, ok := annotationMap["target"].(map[string]interface{}); ok && target != nil { + queryKind := dashv2alpha1.DashboardDataQueryKind{ + Kind: "DataQuery", + Spec: target, + } + // Group field is not available in v2alpha1 DashboardDataQueryKind + query = &queryKind + } else { + // Always provide a query to match frontend behavior + queryKind := dashv2alpha1.DashboardDataQueryKind{ + Kind: "DataQuery", + Spec: map[string]interface{}{}, + } + // Group field is not available in v2alpha1 DashboardDataQueryKind + query = &queryKind + } + + // Transform filter + var filter *dashv2alpha1.DashboardAnnotationPanelFilter + if filterMap, ok := annotationMap["filter"].(map[string]interface{}); ok && filterMap != nil { + filter = buildAnnotationFilter(filterMap) + } + + // Transform builtIn from float64 to bool + var builtInPtr *bool + if builtInVal, ok := annotationMap["builtIn"]; ok && builtInVal != nil { + switch v := builtInVal.(type) { + case float64: + val := v != 0 + builtInPtr = &val + case int: + val := v != 0 + builtInPtr = &val + case bool: + builtInPtr = &v + } + } + + defaultAnnotationQuerySpec := dashv2alpha1.NewDashboardAnnotationQuerySpec() + spec := dashv2alpha1.DashboardAnnotationQuerySpec{ + Name: schemaversion.GetStringValue(annotationMap, "name"), + Datasource: datasourceRef, + Query: query, + Enable: getBoolField(annotationMap, "enable", defaultAnnotationQuerySpec.Enable), // Default to false to match frontend: Boolean(annotation.enable) || Boolean(override?.enable) + Hide: getBoolField(annotationMap, "hide", defaultAnnotationQuerySpec.Hide), + IconColor: schemaversion.GetStringValue(annotationMap, "iconColor", defaultAnnotationQuerySpec.IconColor), + BuiltIn: builtInPtr, + Filter: filter, + } + + // Handle any additional properties in LegacyOptions + legacyOptions := make(map[string]interface{}) + if annotationType := schemaversion.GetStringValue(annotationMap, "type"); annotationType != "" { + legacyOptions["type"] = annotationType + } + + // Add other legacy fields if they exist + for key, value := range annotationMap { + switch key { + case "name", "datasource", "enable", "hide", "iconColor", "filter", "target", "builtIn", "type": + // Skip already handled fields + default: + legacyOptions[key] = value + } + } + + if len(legacyOptions) > 0 { + spec.LegacyOptions = legacyOptions + } + + return dashv2alpha1.DashboardAnnotationQueryKind{ + Kind: "AnnotationQuery", + Spec: spec, + }, nil +} + +func buildAnnotationFilter(filterMap map[string]interface{}) *dashv2alpha1.DashboardAnnotationPanelFilter { + filter := &dashv2alpha1.DashboardAnnotationPanelFilter{ + Ids: []uint32{}, + } + + if exclude := getBoolField(filterMap, "exclude", false); exclude { + filter.Exclude = &exclude + } + + if ids, ok := filterMap["ids"].([]interface{}); ok { + uintIds := make([]uint32, 0, len(ids)) + for _, id := range ids { + switch v := id.(type) { + case float64: + if v >= 0 && v <= float64(^uint32(0)) { + uintIds = append(uintIds, uint32(v)) + } + case int: + if v >= 0 && v <= int(^uint32(0)) { + uintIds = append(uintIds, uint32(v)) + } + } + } + filter.Ids = uintIds + } + + return filter +} + +// Panel helper functions + +func transformPanelQueries(ctx context.Context, panelMap map[string]interface{}, dsInfoProvider schemaversion.DataSourceInfoProvider) []dashv2alpha1.DashboardPanelQueryKind { + targets, ok := panelMap["targets"].([]interface{}) + if !ok { + return []dashv2alpha1.DashboardPanelQueryKind{} + } + + // Get panel datasource + var panelDatasource *dashv2alpha1.DashboardDataSourceRef + if ds, ok := panelMap["datasource"].(map[string]interface{}); ok { + dsUID := schemaversion.GetStringValue(ds, "uid") + dsType := schemaversion.GetStringValue(ds, "type") + + // If we have a UID, use it to get the correct type from the datasource service + // BUT: Don't try to resolve types for template variables + if dsUID != "" && dsType == "" && !isTemplateVariable(dsUID) { + dsType = getDatasourceTypeByUID(ctx, dsUID, dsInfoProvider) + } else if dsUID == "" && dsType == "" { + // If no UID and no type, use default + dsType = getDefaultDatasourceType(ctx, dsInfoProvider) + } + + panelDatasource = &dashv2alpha1.DashboardDataSourceRef{ + Type: &dsType, + Uid: &dsUID, + } + } + + queries := make([]dashv2alpha1.DashboardPanelQueryKind, 0, len(targets)) + + for _, target := range targets { + if targetMap, ok := target.(map[string]interface{}); ok { + query := transformSingleQuery(ctx, targetMap, panelDatasource, dsInfoProvider) + queries = append(queries, query) + } + } + + return queries +} + +func transformSingleQuery(ctx context.Context, targetMap map[string]interface{}, panelDatasource *dashv2alpha1.DashboardDataSourceRef, dsInfoProvider schemaversion.DataSourceInfoProvider) dashv2alpha1.DashboardPanelQueryKind { + refId := schemaversion.GetStringValue(targetMap, "refId", "A") + hidden := getBoolField(targetMap, "hide", false) + + // Extract datasource from query or use panel datasource + var queryDatasourceType string + var queryDatasourceUID string + if ds, ok := targetMap["datasource"].(map[string]interface{}); ok { + queryDatasourceUID = schemaversion.GetStringValue(ds, "uid") + queryDatasourceType = schemaversion.GetStringValue(ds, "type") + + // If we have a UID, use it to get the correct type from the datasource service + // BUT: Don't try to resolve types for template variables + if queryDatasourceUID != "" && queryDatasourceType == "" && !isTemplateVariable(queryDatasourceUID) { + queryDatasourceType = getDatasourceTypeByUID(ctx, queryDatasourceUID, dsInfoProvider) + } + } else if panelDatasource != nil { + // Only use panel datasource if it's not a mixed datasource + // Mixed datasources should not be propagated to individual queries + if panelDatasource.Uid != nil && *panelDatasource.Uid != "-- Mixed --" { + if panelDatasource.Type != nil { + queryDatasourceType = *panelDatasource.Type + } + queryDatasourceUID = *panelDatasource.Uid + } + } + + // Build query spec by excluding known fields + querySpec := make(map[string]interface{}) + for key, value := range targetMap { + switch key { + case "refId", "hide", "datasource": + // Skip these fields as they're handled separately + default: + querySpec[key] = value + } + } + + // Create the panel query spec + panelQuerySpec := dashv2alpha1.DashboardPanelQuerySpec{ + RefId: refId, + Hidden: hidden, + Query: buildDataQueryKind(querySpec, queryDatasourceType), + } + + // Only include datasource reference if UID is provided + if queryDatasourceUID != "" { + panelQuerySpec.Datasource = &dashv2alpha1.DashboardDataSourceRef{ + Type: &queryDatasourceType, + Uid: &queryDatasourceUID, + } + } + + return dashv2alpha1.DashboardPanelQueryKind{ + Kind: "PanelQuery", + Spec: panelQuerySpec, + } +} + +func transformPanelTransformations(panelMap map[string]interface{}) []dashv2alpha1.DashboardTransformationKind { + transformations, ok := panelMap["transformations"].([]interface{}) + if !ok { + return []dashv2alpha1.DashboardTransformationKind{} + } + + result := make([]dashv2alpha1.DashboardTransformationKind, 0, len(transformations)) + for _, t := range transformations { + if tMap, ok := t.(map[string]interface{}); ok { + // Extract the transformation ID + transformationId := schemaversion.GetStringValue(tMap, "id") + + // Extract the options field specifically + var options interface{} + if opts, exists := tMap["options"]; exists { + options = opts + } + + transformationKind := dashv2alpha1.DashboardTransformationKind{ + Kind: transformationId, + Spec: dashv2alpha1.DashboardDataTransformerConfig{ + Id: transformationId, + Options: options, + }, + } + result = append(result, transformationKind) + } + } + + return result +} + +func buildQueryOptions(panelMap map[string]interface{}) dashv2alpha1.DashboardQueryOptionsSpec { + queryOptions := dashv2alpha1.DashboardQueryOptionsSpec{} + + if cacheTimeout := schemaversion.GetStringValue(panelMap, "cacheTimeout"); cacheTimeout != "" { + queryOptions.CacheTimeout = &cacheTimeout + } + if maxDataPoints := getIntField(panelMap, "maxDataPoints", 0); maxDataPoints > 0 { + maxDP := int64(maxDataPoints) + queryOptions.MaxDataPoints = &maxDP + } + if interval := schemaversion.GetStringValue(panelMap, "interval"); interval != "" { + queryOptions.Interval = &interval + } + if hideTimeOverride := getBoolField(panelMap, "hideTimeOverride", false); hideTimeOverride { + queryOptions.HideTimeOverride = &hideTimeOverride + } + // Handle queryCachingTTL as string or number + if val, exists := panelMap["queryCachingTTL"]; exists { + switch v := val.(type) { + case string: + if v != "" { + if ttl, err := strconv.ParseInt(v, 10, 64); err == nil { + queryOptions.QueryCachingTTL = &ttl + } + } + case float64: + ttl := int64(v) + queryOptions.QueryCachingTTL = &ttl + case int: + ttl := int64(v) + queryOptions.QueryCachingTTL = &ttl + } + } + if timeFrom := schemaversion.GetStringValue(panelMap, "timeFrom"); timeFrom != "" { + queryOptions.TimeFrom = &timeFrom + } + if timeShift := schemaversion.GetStringValue(panelMap, "timeShift"); timeShift != "" { + queryOptions.TimeShift = &timeShift + } + + return queryOptions +} + +func transformDataLinks(panelMap map[string]interface{}) []dashv2alpha1.DashboardDataLink { + links, ok := panelMap["links"].([]interface{}) + if !ok { + return []dashv2alpha1.DashboardDataLink{} + } + + result := make([]dashv2alpha1.DashboardDataLink, 0, len(links)) + for _, link := range links { + if linkMap, ok := link.(map[string]interface{}); ok { + dataLink := dashv2alpha1.DashboardDataLink{ + Title: schemaversion.GetStringValue(linkMap, "title"), + Url: schemaversion.GetStringValue(linkMap, "url"), + } + if _, exists := linkMap["targetBlank"]; exists { + targetBlank := getBoolField(linkMap, "targetBlank", false) + dataLink.TargetBlank = &targetBlank + } + + result = append(result, dataLink) + } + } + + return result +} + +func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizConfigKind { + panelType := schemaversion.GetStringValue(panelMap, "type", "timeseries") + pluginVersion := schemaversion.GetStringValue(panelMap, "pluginVersion") + + // Extract field config and options + fieldConfig := make(map[string]interface{}) + if fc, ok := panelMap["fieldConfig"].(map[string]interface{}); ok { + fieldConfig = fc + } + + options := make(map[string]interface{}) + if opts, ok := panelMap["options"].(map[string]interface{}); ok { + options = opts + } + + // Add frontend-style default options to match frontend behavior + if legend, ok := options["legend"].(map[string]interface{}); ok { + // Add showLegend: true to match frontend behavior + showLegend := getBoolField(legend, "showLegend", true) + legend["showLegend"] = showLegend + options["legend"] = legend + } + + // Build field config by mapping each field individually + fieldConfigSource := extractFieldConfigSource(fieldConfig) + + return dashv2alpha1.DashboardVizConfigKind{ + Kind: panelType, // Use panelType as Kind (plugin ID) to match schema comment + Spec: dashv2alpha1.DashboardVizConfigSpec{ + PluginVersion: pluginVersion, + FieldConfig: fieldConfigSource, + Options: options, + }, + } +} + +func extractFieldConfigSource(fieldConfig map[string]interface{}) dashv2alpha1.DashboardFieldConfigSource { + // Always initialize with empty defaults to match frontend behavior + fieldConfigSource := dashv2alpha1.DashboardFieldConfigSource{ + Defaults: dashv2alpha1.DashboardFieldConfig{}, + Overrides: []dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides{}, + } + + if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok { + fieldConfigDefaults := extractFieldConfigDefaults(defaults) + fieldConfigSource.Defaults = fieldConfigDefaults + } + + // Handle overrides + fieldConfigSource.Overrides = extractFieldConfigOverrides(fieldConfig) + + return fieldConfigSource +} + +// Helper functions for extracting field config values +func extractFloat64Field(defaults map[string]interface{}, key string) (*float64, bool) { + if val, exists := defaults[key]; exists { + if floatVal, ok := val.(float64); ok { + return &floatVal, true + } + } + return nil, false +} + +func extractStringField(defaults map[string]interface{}, key string) (*string, bool) { + if val, exists := defaults[key]; exists { + if strVal, ok := val.(string); ok { + return &strVal, true + } + } + return nil, false +} + +func extractBoolField(defaults map[string]interface{}, key string) (*bool, bool) { + if val, exists := defaults[key]; exists { + if boolVal, ok := val.(bool); ok { + return &boolVal, true + } + } + return nil, false +} + +func extractArrayField(defaults map[string]interface{}, key string) ([]interface{}, bool) { + if val, exists := defaults[key]; exists { + if arrayVal, ok := val.([]interface{}); ok { + return arrayVal, true + } + } + return nil, false +} + +func extractMapField(defaults map[string]interface{}, key string) (map[string]interface{}, bool) { + if val, exists := defaults[key]; exists { + if mapVal, ok := val.(map[string]interface{}); ok { + return mapVal, true + } + } + return nil, false +} + +func extractFieldConfigDefaults(defaults map[string]interface{}) dashv2alpha1.DashboardFieldConfig { + fieldConfigDefaults := dashv2alpha1.DashboardFieldConfig{} + hasDefaults := false + + // Extract color + if colorMap, ok := extractMapField(defaults, "color"); ok { + fieldConfigDefaults.Color = buildFieldColor(colorMap) + hasDefaults = true + } + + // Extract custom + if customMap, ok := extractMapField(defaults, "custom"); ok { + fieldConfigDefaults.Custom = customMap + hasDefaults = true + } + + // Extract numeric fields + if val, ok := extractFloat64Field(defaults, "decimals"); ok { + fieldConfigDefaults.Decimals = val + hasDefaults = true + } + if val, ok := extractFloat64Field(defaults, "max"); ok { + fieldConfigDefaults.Max = val + hasDefaults = true + } + if val, ok := extractFloat64Field(defaults, "min"); ok { + fieldConfigDefaults.Min = val + hasDefaults = true + } + + // Extract string fields + if val, ok := extractStringField(defaults, "description"); ok { + fieldConfigDefaults.Description = val + hasDefaults = true + } + if val, ok := extractStringField(defaults, "displayName"); ok { + fieldConfigDefaults.DisplayName = val + hasDefaults = true + } + if val, ok := extractStringField(defaults, "displayNameFromDS"); ok { + fieldConfigDefaults.DisplayNameFromDS = val + hasDefaults = true + } + if val, ok := extractStringField(defaults, "noValue"); ok { + fieldConfigDefaults.NoValue = val + hasDefaults = true + } + if val, ok := extractStringField(defaults, "path"); ok { + fieldConfigDefaults.Path = val + hasDefaults = true + } + if val, ok := extractStringField(defaults, "unit"); ok { + fieldConfigDefaults.Unit = val + hasDefaults = true + } + + // Extract bool fields + if val, ok := extractBoolField(defaults, "filterable"); ok { + fieldConfigDefaults.Filterable = val + hasDefaults = true + } + if val, ok := extractBoolField(defaults, "writeable"); ok { + fieldConfigDefaults.Writeable = val + hasDefaults = true + } + + // Extract array field + if linksArray, ok := extractArrayField(defaults, "links"); ok { + fieldConfigDefaults.Links = linksArray + hasDefaults = true + } + + // Extract mappings + if mappings, exists := defaults["mappings"]; exists { + resultMappings := buildValueMappings(mappings) + fieldConfigDefaults.Mappings = resultMappings + hasDefaults = true + } + + // Extract thresholds + if thresholdsMap, ok := extractMapField(defaults, "thresholds"); ok { + thresholdsConfig := buildThresholdsConfig(thresholdsMap) + fieldConfigDefaults.Thresholds = thresholdsConfig + hasDefaults = true + } + + // Add frontend-style default custom field to match frontend behavior + if !hasDefaults { + fieldConfigDefaults.Custom = map[string]interface{}{} + } + + return fieldConfigDefaults +} + +func buildFieldColor(colorMap map[string]interface{}) *dashv2alpha1.DashboardFieldColor { + var fieldColor dashv2alpha1.DashboardFieldColor + + // mode (required) + if mode, ok := colorMap["mode"].(string); ok { + fieldColor.Mode = dashv2alpha1.DashboardFieldColorModeId(mode) + } + + // fixedColor (optional) + if fixedColor, ok := colorMap["fixedColor"].(string); ok { + fieldColor.FixedColor = &fixedColor + } + + // seriesBy (optional) + if seriesBy, ok := colorMap["seriesBy"].(string); ok { + sb := dashv2alpha1.DashboardFieldColorSeriesByMode(seriesBy) + fieldColor.SeriesBy = &sb + } + + return &fieldColor +} + +func buildValueMappings(mappings interface{}) []dashv2alpha1.DashboardValueMapping { + var resultMappings []dashv2alpha1.DashboardValueMapping + + mappingsArr, ok := mappings.([]interface{}) + if !ok || len(mappingsArr) == 0 { + return resultMappings + } + + for _, mapping := range mappingsArr { + mappingMap, ok := mapping.(map[string]interface{}) + if !ok { + continue + } + + typ, ok := mappingMap["type"].(string) + if !ok { + continue + } + + switch typ { + case "value": + if valueMapping := buildValueMap(mappingMap); valueMapping != nil { + resultMappings = append(resultMappings, dashv2alpha1.DashboardValueMapping{ + ValueMap: valueMapping, + }) + } + case "range": + if rangeMapping := buildRangeMap(mappingMap); rangeMapping != nil { + resultMappings = append(resultMappings, dashv2alpha1.DashboardValueMapping{ + RangeMap: rangeMapping, + }) + } + case "regex": + if regexMapping := buildRegexMap(mappingMap); regexMapping != nil { + resultMappings = append(resultMappings, dashv2alpha1.DashboardValueMapping{ + RegexMap: regexMapping, + }) + } + case "special": + if specialMapping := buildSpecialValueMap(mappingMap); specialMapping != nil { + resultMappings = append(resultMappings, dashv2alpha1.DashboardValueMapping{ + SpecialValueMap: specialMapping, + }) + } + } + } + + return resultMappings +} + +func buildValueMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardValueMap { + valMap := &dashv2alpha1.DashboardValueMap{} + valMap.Type = dashv2alpha1.DashboardMappingTypeValue + + opts, ok := mappingMap["options"].(map[string]interface{}) + if !ok { + return valMap + } + + valMap.Options = make(map[string]dashv2alpha1.DashboardValueMappingResult) + for k, v := range opts { + resMap, ok := v.(map[string]interface{}) + if !ok { + continue + } + res := buildValueMappingResult(resMap) + valMap.Options[k] = res + } + + return valMap +} + +func buildRangeMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardRangeMap { + rangeMap := &dashv2alpha1.DashboardRangeMap{} + rangeMap.Type = dashv2alpha1.DashboardMappingTypeRange + + opts, ok := mappingMap["options"].(map[string]interface{}) + if !ok || len(opts) == 0 { + return nil + } + + // Return nil only if both "from" and "to" are missing + if opts["from"] == nil && opts["to"] == nil { + return nil + } + + r := dashv2alpha1.DashboardV2alpha1RangeMapOptions{} + if from, ok := opts["from"].(float64); ok { + r.From = &from + } + if to, ok := opts["to"].(float64); ok { + r.To = &to + } + + // Result is a DashboardValueMappingResult + if resMap, ok := opts["result"].(map[string]interface{}); ok { + r.Result = buildValueMappingResult(resMap) + } + + rangeMap.Options = r + return rangeMap +} + +func buildRegexMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardRegexMap { + regexMap := &dashv2alpha1.DashboardRegexMap{} + regexMap.Type = dashv2alpha1.DashboardMappingTypeRegex + + opts, ok := mappingMap["options"].([]interface{}) + if !ok || len(opts) == 0 { + return nil + } + + optMap, ok := opts[0].(map[string]interface{}) + if !ok { + return nil + } + + r := dashv2alpha1.DashboardV2alpha1RegexMapOptions{} + if pattern, ok := optMap["regex"].(string); ok { + r.Pattern = pattern + } + + // Result is a DashboardValueMappingResult + if resMap, ok := optMap["result"].(map[string]interface{}); ok { + r.Result = buildValueMappingResult(resMap) + } + + regexMap.Options = r + return regexMap +} + +func buildSpecialValueMap(mappingMap map[string]interface{}) *dashv2alpha1.DashboardSpecialValueMap { + specialMap := &dashv2alpha1.DashboardSpecialValueMap{} + specialMap.Type = dashv2alpha1.DashboardMappingTypeSpecial + + opts, ok := mappingMap["options"].(map[string]interface{}) + if !ok || len(opts) == 0 { + return nil + } + + r := dashv2alpha1.DashboardV2alpha1SpecialValueMapOptions{} + if match, ok := opts["match"].(string); ok { + r.Match = dashv2alpha1.DashboardSpecialValueMatch(match) + } + + // Result is a DashboardValueMappingResult + if resMap, ok := opts["result"].(map[string]interface{}); ok { + r.Result = buildValueMappingResult(resMap) + } + + specialMap.Options = r + return specialMap +} + +func buildValueMappingResult(resMap map[string]interface{}) dashv2alpha1.DashboardValueMappingResult { + res := dashv2alpha1.DashboardValueMappingResult{} + + if text, ok := resMap["text"].(string); ok { + res.Text = &text + } + if color, ok := resMap["color"].(string); ok { + res.Color = &color + } + if icon, ok := resMap["icon"].(string); ok { + res.Icon = &icon + } + if idx, ok := resMap["index"].(float64); ok { + idx32 := int32(idx) + res.Index = &idx32 + } + + return res +} + +func buildThresholdsConfig(thresholdsMap map[string]interface{}) *dashv2alpha1.DashboardThresholdsConfig { + thresholdsConfig := &dashv2alpha1.DashboardThresholdsConfig{} + + // Convert mode + if mode, ok := thresholdsMap["mode"].(string); ok { + thresholdsConfig.Mode = dashv2alpha1.DashboardThresholdsMode(mode) + } + + // Convert steps + if steps, ok := thresholdsMap["steps"].([]interface{}); ok { + thresholdsConfig.Steps = make([]dashv2alpha1.DashboardThreshold, 0, len(steps)) + for _, step := range steps { + if stepMap, ok := step.(map[string]interface{}); ok { + threshold := dashv2alpha1.DashboardThreshold{} + // Handle threshold value: null means -Infinity + if value, exists := stepMap["value"]; exists { + if value == nil { + // null means -Infinity + threshold.Value = nil + } else if floatVal, ok := value.(float64); ok { + threshold.Value = &floatVal + } + } else { + // Value not present, leave as nil (represents -Infinity) + threshold.Value = nil + } + if color, ok := stepMap["color"].(string); ok { + threshold.Color = color + } + thresholdsConfig.Steps = append(thresholdsConfig.Steps, threshold) + } + } + } + + return thresholdsConfig +} + +func extractFieldConfigOverrides(fieldConfig map[string]interface{}) []dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides { + overrides, ok := fieldConfig["overrides"].([]interface{}) + if !ok || len(overrides) == 0 { + // Use empty array to match frontend behavior + return []dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides{} + } + + result := make([]dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides, 0, len(overrides)) + for _, override := range overrides { + overrideMap, ok := override.(map[string]interface{}) + if !ok { + continue + } + + fieldOverride := dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides{} + + if systemRef, ok := overrideMap["__systemRef"].(string); ok { + fieldOverride.SystemRef = &systemRef + } + + // Map override fields individually + if matcher, exists := overrideMap["matcher"]; exists { + if matcherMap, ok := matcher.(map[string]interface{}); ok { + fieldOverride.Matcher = dashv2alpha1.DashboardMatcherConfig{ + Id: schemaversion.GetStringValue(matcherMap, "id"), + Options: matcherMap["options"], + } + } + } + if properties, exists := overrideMap["properties"]; exists { + if propertiesArray, ok := properties.([]interface{}); ok { + fieldOverride.Properties = make([]dashv2alpha1.DashboardDynamicConfigValue, 0, len(propertiesArray)) + for _, property := range propertiesArray { + if propertyMap, ok := property.(map[string]interface{}); ok { + fieldOverride.Properties = append(fieldOverride.Properties, dashv2alpha1.DashboardDynamicConfigValue{ + Id: schemaversion.GetStringValue(propertyMap, "id"), + Value: propertyMap["value"], + }) + } + } + } + } + + result = append(result, fieldOverride) + } + + return result +} diff --git a/apps/dashboard/pkg/migration/conversion/v2.go b/apps/dashboard/pkg/migration/conversion/v2.go index 91fcd8cf07a..db1824e725c 100644 --- a/apps/dashboard/pkg/migration/conversion/v2.go +++ b/apps/dashboard/pkg/migration/conversion/v2.go @@ -27,7 +27,7 @@ func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, s return nil } -func Convert_V2alpha1_to_V1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { +func Convert_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta // TODO: implement V2 to V1 conversion @@ -89,7 +89,7 @@ func Convert_V2beta1_to_V0(in *dashv2beta1.Dashboard, out *dashv0.Dashboard, sco return nil } -func Convert_V2beta1_to_V1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { +func Convert_V2beta1_to_V1beta1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta // TODO: implement v2beta1 to V1 conversion diff --git a/apps/dashboard/pkg/migration/conversion/v2_test.go b/apps/dashboard/pkg/migration/conversion/v2_test.go new file mode 100644 index 00000000000..fa3b947a2d0 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2_test.go @@ -0,0 +1,42 @@ +package conversion + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" +) + +// TestV2alpha1ConversionErrorHandling tests that v2alpha1 conversion functions properly handle errors +func TestV2alpha1ConversionErrorHandling(t *testing.T) { + t.Run("Convert_V2alpha1_to_V2beta1 sets status correctly on success", func(t *testing.T) { + source := &dashv2alpha1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-dashboard", + }, + Spec: dashv2alpha1.DashboardSpec{ + Title: "test dashboard", + Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{}, + }, + }, + }, + } + target := &dashv2beta1.Dashboard{} + + err := Convert_V2alpha1_to_V2beta1(source, target, nil) + + require.NoError(t, err) + // Verify success status is set correctly + require.NotNil(t, target.Status.Conversion) + require.False(t, target.Status.Conversion.Failed) + require.Equal(t, dashv2alpha1.VERSION, *target.Status.Conversion.StoredVersion) + require.Nil(t, target.Status.Conversion.Error) + }) +} diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index ec673c80137..9243d246cf0 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -125,22 +125,27 @@ func convertDataQuery_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardDataQueryKin if in == nil { // v2beta1 requires a query even if v2alpha1 had none, so create a default out.Kind = "DataQuery" - out.Group = "" out.Version = "v0" out.Spec = make(map[string]interface{}) + // Don't set group field for manual annotations (no datasource) } else { out.Kind = "DataQuery" - out.Group = in.Kind out.Version = "v0" out.Spec = in.Spec + // Don't set group field by default - only set if datasource is present } - // Convert datasource reference + // Convert datasource reference and set group from datasource type if datasource != nil { out.Datasource = &dashv2beta1.DashboardV2beta1DataQueryKindDatasource{} if datasource.Uid != nil { out.Datasource.Name = datasource.Uid } + // Set group to datasource type to match frontend behavior + // Only set group if datasource type is not empty (matches frontend logic) + if datasource.Type != nil && *datasource.Type != "" { + out.Group = *datasource.Type + } } return nil @@ -256,7 +261,7 @@ func convertQueryOptions_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQueryOpti func convertVizConfig_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVizConfigKind, out *dashv2beta1.DashboardVizConfigKind) error { out.Kind = "VizConfig" - out.Group = in.Kind + out.Group = in.Kind // in.Kind is the panel type (e.g., "timeseries", "graph") out.Version = in.Spec.PluginVersion out.Spec = dashv2beta1.DashboardVizConfigSpec{ Options: in.Spec.Options, @@ -303,6 +308,7 @@ func convertFieldConfig_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardFieldConfi Steps: make([]dashv2beta1.DashboardThreshold, len(in.Thresholds.Steps)), } for i, step := range in.Thresholds.Steps { + // Convert threshold values: preserve null values from v2alpha1 out.Thresholds.Steps[i] = dashv2beta1.DashboardThreshold{ Value: step.Value, Color: step.Color, @@ -329,6 +335,8 @@ func convertFieldConfig_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardFieldConfi } func convertFieldConfigOverride_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides, out *dashv2beta1.DashboardV2beta1FieldConfigSourceOverrides) { + out.SystemRef = in.SystemRef + out.Matcher = dashv2beta1.DashboardMatcherConfig{ Id: in.Matcher.Id, Options: in.Matcher.Options, @@ -629,6 +637,7 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind, } if in.IntervalVariableKind != nil { + defaultIntervalVariableSpec := dashv2beta1.NewDashboardIntervalVariableSpec() out.IntervalVariableKind = &dashv2beta1.DashboardIntervalVariableKind{ Kind: in.IntervalVariableKind.Kind, Spec: dashv2beta1.DashboardIntervalVariableSpec{ @@ -639,7 +648,7 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind, Auto: in.IntervalVariableKind.Spec.Auto, AutoMin: in.IntervalVariableKind.Spec.AutoMin, AutoCount: in.IntervalVariableKind.Spec.AutoCount, - Refresh: dashv2beta1.DashboardVariableRefresh(in.IntervalVariableKind.Spec.Refresh), + Refresh: defaultIntervalVariableSpec.Refresh, Label: in.IntervalVariableKind.Spec.Label, Hide: dashv2beta1.DashboardVariableHide(in.IntervalVariableKind.Spec.Hide), SkipUrlSync: in.IntervalVariableKind.Spec.SkipUrlSync, @@ -690,11 +699,19 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind, } if in.AdhocVariableKind != nil { + group := "" + var uid *string + if in.AdhocVariableKind.Spec.Datasource != nil { + if in.AdhocVariableKind.Spec.Datasource.Type != nil { + group = *in.AdhocVariableKind.Spec.Datasource.Type + } + uid = in.AdhocVariableKind.Spec.Datasource.Uid + } out.AdhocVariableKind = &dashv2beta1.DashboardAdhocVariableKind{ Kind: in.AdhocVariableKind.Kind, - Group: *in.AdhocVariableKind.Spec.Datasource.Type, + Group: group, Datasource: &dashv2beta1.DashboardV2beta1AdhocVariableKindDatasource{ - Name: in.AdhocVariableKind.Spec.Datasource.Uid, + Name: uid, }, Spec: dashv2beta1.DashboardAdhocVariableSpec{ Name: in.AdhocVariableKind.Spec.Name, diff --git a/apps/dashboard/pkg/migration/frontend_defaults.go b/apps/dashboard/pkg/migration/frontend_defaults.go index c376be6f47f..1df28c4de27 100644 --- a/apps/dashboard/pkg/migration/frontend_defaults.go +++ b/apps/dashboard/pkg/migration/frontend_defaults.go @@ -440,10 +440,7 @@ func getPanels(dashboard map[string]interface{}) []map[string]interface{} { func cleanupPanelForSaveWithContext(panel map[string]interface{}, isNested bool) { // Apply auto-migration logic (matches frontend PanelModel constructor) // This happens during cleanup phase to match when frontend applies auto-migration - // Only apply auto-migration to top-level panels, not nested ones (matches frontend behavior) - if !isNested { - applyPanelAutoMigration(panel) - } + applyPanelAutoMigration(panel) // Library panel specific cleanup (matches frontend behavior) // Frontend only preserves id, title, gridPos, and libraryPanel for library panels diff --git a/apps/dashboard/pkg/migration/migrate.go b/apps/dashboard/pkg/migration/migrate.go index 08cba231fd3..9c887b553c6 100644 --- a/apps/dashboard/pkg/migration/migrate.go +++ b/apps/dashboard/pkg/migration/migrate.go @@ -13,11 +13,20 @@ func Initialize(dsInfoProvider schemaversion.DataSourceInfoProvider) { migratorInstance.init(dsInfoProvider) } +// GetDataSourceInfoProvider returns the datasource info provider instance that was initialized. +// This allows reuse of the same provider instance across migrations and conversions. +func GetDataSourceInfoProvider() schemaversion.DataSourceInfoProvider { + // Wait for initialization to complete + <-migratorInstance.ready + return migratorInstance.dsInfoProvider +} + // ResetForTesting resets the migrator singleton for testing purposes. func ResetForTesting() { migratorInstance = &migrator{ - migrations: map[int]schemaversion.SchemaVersionMigrationFunc{}, - ready: make(chan struct{}), + migrations: map[int]schemaversion.SchemaVersionMigrationFunc{}, + ready: make(chan struct{}), + dsInfoProvider: nil, } initOnce = sync.Once{} } @@ -37,12 +46,14 @@ var ( ) type migrator struct { - ready chan struct{} - migrations map[int]schemaversion.SchemaVersionMigrationFunc + ready chan struct{} + migrations map[int]schemaversion.SchemaVersionMigrationFunc + dsInfoProvider schemaversion.DataSourceInfoProvider } func (m *migrator) init(dsInfoProvider schemaversion.DataSourceInfoProvider) { initOnce.Do(func() { + m.dsInfoProvider = dsInfoProvider m.migrations = schemaversion.GetMigrations(dsInfoProvider) close(m.ready) }) diff --git a/apps/dashboard/pkg/migration/schemaversion/migration_utils.go b/apps/dashboard/pkg/migration/schemaversion/migration_utils.go index b3bd79d6fd4..64dfc61589e 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migration_utils.go +++ b/apps/dashboard/pkg/migration/schemaversion/migration_utils.go @@ -8,12 +8,15 @@ import ( // migration_utils.go contains shared utility functions used across multiple schema version migrations. // GetStringValue safely extracts a string value from a map, returning empty string if not found or not a string -func GetStringValue(m map[string]interface{}, key string) string { +func GetStringValue(m map[string]interface{}, key string, defaultValue ...string) string { if value, ok := m[key]; ok { if s, ok := value.(string); ok { return s } } + if len(defaultValue) > 0 { + return defaultValue[0] + } return "" } diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/annotations/multi-lane-annotations.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/annotations/multi-lane-annotations.v42.json new file mode 100644 index 00000000000..f77cb68c01f --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/annotations/multi-lane-annotations.v42.json @@ -0,0 +1,1024 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "maxDataPoints": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "noise": 10, + "refId": "A", + "scenarioId": "random_walk", + "spread": 10 + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"fields\": [\n {\n \"name\": \"type\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"color\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"time\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1720697881000,\n 1728580067000,\n 1730129182000,\n 1730734644000,\n 1732542118000,\n 1736426576000,\n 1736874473000,\n 1738676647000,\n 1740067713000,\n 1740684246000,\n 1743733609000,\n 1744034815000,\n 1745335229000,\n 1745936130000,\n 1746107356000,\n 1747055303000,\n 1747946736000,\n 1748446531000,\n 1750098297000,\n 1750963080000,\n 1750963096000,\n 1752067555000,\n 1752080606000,\n 1753125884000,\n 1754395568000,\n 1754407010000,\n 1756228450000,\n 1757947221000,\n 1759763050000,\n 1759924632000,\n 1761572602000\n ],\n \"type\": \"time\"\n },\n {\n \"name\": \"timeEnd\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1729081505000,\n 1730129307000,\n 1730838127000,\n 1732543887000,\n 1736528756000,\n 1736874600000,\n 1740064375000,\n 1740170732000,\n 1742397235000,\n 1743733611000,\n 1744038997000,\n 1745341972000,\n 1745936141000,\n 1746118905000,\n 1747055307000,\n 1747853613000,\n 1748446520000,\n 1750187872000,\n 1750963071000,\n 1751038503000,\n 1752067563000,\n 1752166243000,\n 1753396603000,\n 1754395592000,\n 1754407004000,\n 1756228445000,\n 1758134868000,\n 1759763044000,\n 1761572592000,\n 1761572593000,\n null\n ],\n \"type\": \"number\"\n },\n {\n \"name\": \"title\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"0.1.0\",\n \"1.0.2\",\n \"1.0.3\",\n \"1.0.4\",\n \"1.0.5\",\n \"1.0.6\",\n \"1.0.7\",\n \"1.0.8\",\n \"1.0.9\",\n \"1.0.10\",\n \"1.0.11\",\n \"1.0.12\",\n \"1.0.13\",\n \"1.0.14\",\n \"1.0.15\",\n \"1.0.16\",\n \"1.0.17\",\n \"1.0.18\",\n \"1.0.19\",\n \"1.0.20\",\n \"1.0.21\",\n \"1.0.22\",\n \"1.0.23\",\n \"1.0.24\",\n \"1.0.25\",\n \"1.0.26\",\n \"1.0.27\",\n \"1.0.28\",\n \"1.0.29\",\n \"1.0.30\",\n \"1.0.31\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"text\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n false\n ],\n \"type\": \"boolean\"\n },\n {\n \"name\": \"source\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n {\n \"datasource\": {\n \"type\": \"grafana-github-datasource\",\n \"uid\": \"feyypehpl45xcf\"\n },\n \"enable\": true,\n \"hide\": false,\n \"iconColor\": \"red\",\n \"mappings\": {\n \"text\": {\n \"source\": \"field\",\n \"value\": \"closed\"\n },\n \"time\": {\n \"source\": \"field\",\n \"value\": \"created_at\"\n },\n \"timeEnd\": {\n \"source\": \"field\",\n \"value\": \"closed_at\"\n },\n \"title\": {\n \"source\": \"field\",\n \"value\": \"title\"\n }\n },\n \"name\": \"Milestones\",\n \"target\": {\n \"options\": {\n \"query\": \"\"\n },\n \"owner\": \"grafana\",\n \"queryType\": \"Milestones\",\n \"refId\": \"Anno\",\n \"repository\": \"logs-drilldown\"\n }\n },\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"isRegion\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n false\n ],\n \"type\": \"boolean\"\n }\n ],\n \"length\": 31,\n \"meta\": {\n \"dataTopic\": \"annotations\"\n }\n },\n {\n \"fields\": [\n {\n \"name\": \"type\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Issues\",\n \"Issues\",\n \"Issues\",\n \"Issues\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"color\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"#FADE2A\",\n \"#FADE2A\",\n \"#FADE2A\",\n \"#FADE2A\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"time\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1760112753000,\n 1759930212000,\n 1759924899000,\n 1759850404000\n ],\n \"type\": \"time\"\n },\n {\n \"name\": \"timeEnd\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1760972421000,\n null,\n null,\n 1759926839000\n ],\n \"type\": \"number\"\n },\n {\n \"name\": \"title\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"[BUG]: Error when data source configured for multi tenant queries\",\n \"[FEAT]: Allow users without explore permissions to use logs drilldown\",\n \"Sync Logs panel and label/fields empty states\",\n \"fix: labels clear variable state not showing\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"text\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"[BUG]: Error when data source configured for multi tenant queries\",\n \"[FEAT]: Allow users without explore permissions to use logs drilldown\",\n \"Sync Logs panel and label/fields empty states\",\n \"fix: labels clear variable state not showing\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"tags\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n [\n \"bug\",\n \"needs-triage\"\n ],\n [\n \"enhancement\",\n \"needs-triage\"\n ],\n [\n \"needs-triage\"\n ],\n [\n \"bug\"\n ]\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"source\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n {\n \"datasource\": {\n \"type\": \"grafana-github-datasource\",\n \"uid\": \"feyypehpl45xcf\"\n },\n \"enable\": true,\n \"hide\": false,\n \"iconColor\": \"yellow\",\n \"mappings\": {\n \"tags\": {\n \"source\": \"field\",\n \"value\": \"labels\"\n },\n \"time\": {\n \"source\": \"field\",\n \"value\": \"created_at\"\n },\n \"timeEnd\": {\n \"source\": \"field\",\n \"value\": \"closed_at\"\n }\n },\n \"name\": \"Issues\",\n \"target\": {\n \"options\": {\n \"query\": \"\"\n },\n \"owner\": \"grafana\",\n \"queryType\": \"Issues\",\n \"refId\": \"Anno\",\n \"repository\": \"logs-drilldown\"\n }\n },\n null,\n null,\n null\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"isRegion\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n true,\n false,\n false,\n true\n ],\n \"type\": \"boolean\"\n }\n ],\n \"length\": 4,\n \"meta\": {\n \"dataTopic\": \"annotations\"\n }\n },\n {\n \"fields\": [\n {\n \"name\": \"type\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Pulls\",\n \"Pulls\",\n \"Pulls\",\n \"Pulls\",\n \"Pulls\",\n \"Pulls\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"color\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"#5794F2\",\n \"#5794F2\",\n \"#5794F2\",\n \"#5794F2\",\n \"#5794F2\",\n \"#5794F2\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"time\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1759941908000,\n 1759854730000,\n 1759852733000,\n 1759841416000,\n 1759786844000,\n 1759762979000\n ],\n \"type\": \"time\"\n },\n {\n \"name\": \"timeEnd\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1760110687000,\n 1759857566000,\n 1759926838000,\n 1759843529000,\n 1759848237000,\n 1759769543000\n ],\n \"type\": \"number\"\n },\n {\n \"name\": \"title\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"feat(EmptyLogs): add button to fix with assistant\",\n \"docs: Update troubleshooting page\",\n \"fix: unexpected clear variable behavior\",\n \"docs: update stale readme, fix docker install script\",\n \"docs: Update install and troubleshooting\",\n \"fix: fix RegExp.source removing flags, use toString instead\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"text\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"feat(EmptyLogs): add button to fix with assistant\",\n \"docs: Update troubleshooting page\",\n \"fix: unexpected clear variable behavior\",\n \"docs: update stale readme, fix docker install script\",\n \"docs: Update install and troubleshooting\",\n \"fix: fix RegExp.source removing flags, use toString instead\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"source\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n {\n \"datasource\": {\n \"type\": \"grafana-github-datasource\",\n \"uid\": \"feyypehpl45xcf\"\n },\n \"enable\": true,\n \"hide\": false,\n \"iconColor\": \"blue\",\n \"mappings\": {\n \"time\": {\n \"source\": \"field\",\n \"value\": \"created_at\"\n },\n \"timeEnd\": {\n \"source\": \"field\",\n \"value\": \"merged_at\"\n }\n },\n \"name\": \"Pulls\",\n \"target\": {\n \"options\": {\n \"query\": \"\",\n \"timeField\": 1\n },\n \"owner\": \"grafana\",\n \"queryType\": \"Pull_Requests\",\n \"refId\": \"Anno\",\n \"repository\": \"logs-drilldown\"\n }\n },\n null,\n null,\n null,\n null,\n null\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"isRegion\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n true,\n true,\n true,\n true,\n true,\n true\n ],\n \"type\": \"boolean\"\n }\n ],\n \"length\": 6,\n \"meta\": {\n \"dataTopic\": \"annotations\"\n }\n },\n {\n \"fields\": [\n {\n \"name\": \"type\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"color\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"time\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1760349982000,\n 1760110687000,\n 1759950041000,\n 1759948586000,\n 1759933371000,\n 1759926838000,\n 1759857566000,\n 1759850172000,\n 1759848237000,\n 1759843529000,\n 1759769543000,\n 1759762725000\n ],\n \"type\": \"time\"\n },\n {\n \"name\": \"title\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Matias Chomicki\",\n \"Matias Chomicki\",\n \"github-actions[bot]\",\n \"Liza Detrick\",\n \"grafana-plugins-platform-bot[bot]\",\n \"Galen Kistler\",\n \"J Stickler\",\n \"Piotr Jamróz\",\n \"J Stickler\",\n \"Galen Kistler\",\n \"Galen Kistler\",\n \"Galen Kistler\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"text\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"feat(LogsListScene): add defaultDisplayedFields support (#1554)\\n\\n* feat(LogsListScene): add defaultDisplayedFields support\\n\\n* chore: update mock\\n\\n* fix(Table): omit unsupported field\\n\\n* fix(LogListScene): reset default displayed fields when switching viz\\n\\n* chore: lint\",\n \"feat(EmptyLogs): add button to fix with assistant (#1571)\\n\\n* feat(LogListScene): add error type to identify empty results\\n\\n* feat(LogsPanelError): use assistant to investigate no results\\n\\n* fix(Shoo): shoo\\n\\n* feat(EmbeddedLogs): customize empty logs prompt\\n\\n* feat(LogsPanelError): implement custom CTA\\n\\n* chore: spelling\\n\\n* chore: add unit test\\n\\n* chore: remove it.only\\n\\n* fix(variableHelpers): exclude primary label from variables to clear\\n\\n* feat(embedding): refactor options\\n\\n* feat(assistant): create service\\n\\n* feat(assistant): integrate with empty layout and no matching labels\\n\\n* chore: fix types\\n\\n* chore: update tests\\n\\n* chore: remove quotes\\n\\n* chore: prevent access to an undefined object\\n\\n* chore: move label to constants\\n\\n* Revert \\\"chore: move label to constants\\\"\\n\\nThis reverts commit fbdf96415e5b2af7a5cae4eec6e6d5a30bc05d1c.\\n\\n* chore: use name instead of label\\n\\n* Revert \\\"fix(Shoo): shoo\\\"\\n\\nThis reverts commit 75993f7a845b514cb5da9667e94a2f0a3c913f24.\\n\\n* chore: more unit tests\\n\\n* test(variableHelpers): test getVariablesThatCanBeCleared\",\n \"chore: bump @grafana/create-plugin configuration to 5.26.9 (#1559)\",\n \"feat(table): preferences (#1534)\",\n \"chore(version): bump version to v1.0.29\",\n \"fix: unexpected clear variable behavior (#1567)\\n\\n* fix: show clear ui when query contains clearable label filters\",\n \"docs: Update troubleshooting page (#1568)\",\n \"fix: validate primary label correctly (#1561)\\n\\n* fix: properly clear variables for legacy urls\",\n \"docs: Update install and troubleshooting (#1564)\\n\\nCo-authored-by: Galen Kistler \u003c109082771+gtk-grafana@users.noreply.github.com\u003e\",\n \"docs: update stale readme, fix docker install script (#1565)\\n\\n* docs: update stale readme\\n\\n* chore: fix docker script\",\n \"fix: fix RegExp.source removing flags, use toString instead (#1563)\\n\\n* fix: fix RegExp.source removing flags, use toString instead\",\n \"fix: stale urls (#1562)\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"id\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"cf08043276597abe16f4ae9dbe97bd988d31b7ed\",\n \"01e343dacde3b9fcd862ec2cb2b04fbea4eb6d63\",\n \"9e67734938b980a80496d4cdc60efe7dbc4ba74e\",\n \"924ebd235b6ea99d297d8e04e08ac8ea90a32968\",\n \"23b9b5dc0504a4c397b326562cb796cfdc0531df\",\n \"089fdfa26c7daad5543be1aae2377850348663c3\",\n \"437a568a7a7279c436a674c7b1b0d11a4971d47b\",\n \"ee12a20b32db26b82fe32ed9d5868bd2dde4a4d0\",\n \"657880b399682101616b3413e04c2b664d018ef3\",\n \"fc776d767ebf2a64fb9fad7ab0f479cdaf536cdb\",\n \"a9207d235af9b2f5a4915f072801128a41c4063b\",\n \"e2eae8b8f45dc685659689f85962b0f0a58f13b1\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"source\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n {\n \"datasource\": {\n \"type\": \"grafana-github-datasource\",\n \"uid\": \"feyypehpl45xcf\"\n },\n \"enable\": true,\n \"hide\": false,\n \"iconColor\": \"green\",\n \"mappings\": {\n \"text\": {\n \"source\": \"field\",\n \"value\": \"message\"\n },\n \"time\": {\n \"source\": \"field\",\n \"value\": \"committed_at\"\n },\n \"title\": {\n \"source\": \"field\",\n \"value\": \"author\"\n }\n },\n \"name\": \"Commits\",\n \"target\": {\n \"options\": {\n \"gitRef\": \"main\"\n },\n \"owner\": \"grafana\",\n \"queryType\": \"Commits\",\n \"refId\": \"Anno\",\n \"repository\": \"logs-drilldown\"\n }\n },\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"isRegion\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false\n ],\n \"type\": \"boolean\"\n }\n ],\n \"length\": 12,\n \"meta\": {\n \"dataTopic\": \"annotations\"\n }\n }\n]", + "refId": "B", + "scenarioId": "raw_frame" + } + ], + "title": "Time series", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "annotations": { + "multiLane": true + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Time series (multi-lane)", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "candleStyle": "candles", + "colorStrategy": "open-close", + "colors": { + "down": "red", + "up": "green" + }, + "includeAllFields": false, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mode": "candles+volume", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Candlestick", + "type": "candlestick" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "annotations": { + "multiLane": true + }, + "candleStyle": "candles", + "colorStrategy": "open-close", + "colors": { + "down": "red", + "up": "green" + }, + "includeAllFields": false, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mode": "candles+volume", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Candlestick (multi-lane)", + "type": "candlestick" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "State timeline", + "type": "state-timeline" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "alignValue": "left", + "annotations": { + "multiLane": true + }, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "State timeline (multi-lane)", + "type": "state-timeline" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 7, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Heatmap", + "type": "heatmap" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 8, + "options": { + "annotations": { + "multiLane": true + }, + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Heatmap (multi-lane)", + "type": "heatmap" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 9, + "options": { + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Status history", + "type": "status-history" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 10, + "options": { + "annotations": { + "multiLane": true + }, + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Status history (multi-lane)", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 11, + "maxDataPoints": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"type\": \"timeseries\"\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"config\": {\n \"interval\": 604800000\n }\n },\n {\n \"name\": \"A-series\",\n \"type\": \"number\",\n \"labels\": {},\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1755870318505,\n 1756475118505,\n 1757079918505,\n 1757684718505,\n 1758289518505,\n 1758894318505,\n 1759499118505,\n 1760103918505,\n 1760708718505,\n 1761313518505\n ],\n [\n 49.57457814271496,\n 64.78808691382616,\n 94.88860442042386,\n 96.59132232810856,\n 58.57144477681538,\n 79.33618638515327,\n 89.64117713117561,\n 134.51905322565585,\n 122.83710544843791,\n 79.84039369237018\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"B\",\n \"name\": \"xymark\",\n \"meta\": {\n \"dataTopic\": \"annotations\"\n },\n \"fields\": [\n {\n \"name\": \"xMin\",\n \"type\": \"time\",\n \"config\": {}\n },\n {\n \"name\": \"xMax\",\n \"type\": \"time\",\n \"config\": {}\n },\n {\n \"name\": \"yMin\",\n \"type\": \"number\",\n \"config\": {}\n },\n {\n \"name\": \"yMax\",\n \"type\": \"number\",\n \"config\": {}\n },\n {\n \"name\": \"color\",\n \"type\": \"string\",\n \"config\": {}\n },\n {\n \"name\": \"fillOpacity\",\n \"type\": \"number\",\n \"config\": {}\n },\n {\n \"name\": \"lineWidth\",\n \"type\": \"number\",\n \"config\": {}\n },\n {\n \"name\": \"lineStyle\",\n \"type\": \"string\",\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1757684718505\n ],\n [\n 1758894318505\n ],\n [\n 70\n ],\n [\n 120\n ],\n [\n \"#f00\"\n ],\n [\n 0.1\n ],\n [\n 1\n ],\n [\n \"dash\"\n ]\n ]\n }\n }\n]", + "refId": "B", + "scenarioId": "raw_frame" + } + ], + "title": "xymark", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "templating": { + "list": [] + }, + "time": { + "from": "2025-08-22T13:45:18.505Z", + "to": "2025-10-28T04:38:37.130Z" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Multi-lane annotations", + "uid": "ad7p5pj", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index a85230fd842..863d7b2a102 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -36,8 +36,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -95,13 +94,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -117,8 +115,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -176,13 +173,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -198,8 +194,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -257,13 +252,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -279,8 +273,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -338,13 +331,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -360,8 +352,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -419,13 +410,12 @@ "showThresholdMarkers": false, "sparkline": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -441,8 +431,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -500,13 +489,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -522,8 +510,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -581,13 +568,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -616,8 +602,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -675,13 +660,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -697,8 +681,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -756,13 +739,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -778,8 +760,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -837,13 +818,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -859,8 +839,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -918,13 +897,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -953,8 +931,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1016,13 +993,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1034,8 +1010,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1097,13 +1072,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1115,8 +1089,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1178,13 +1151,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1196,8 +1168,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1259,13 +1230,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1277,8 +1247,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1340,13 +1309,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1371,8 +1339,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1438,13 +1405,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1456,8 +1422,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1523,13 +1488,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1541,8 +1505,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1608,13 +1571,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1639,8 +1601,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -1704,12 +1665,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 98, @@ -1727,8 +1687,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -1792,12 +1751,11 @@ "sparkline": true, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 98, @@ -1828,8 +1786,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -1894,12 +1851,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 8, @@ -1917,8 +1873,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -1984,12 +1939,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 12, @@ -2007,8 +1961,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -2072,12 +2025,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 100, @@ -2095,8 +2047,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -2160,12 +2111,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 100, diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-heatmap/heatmap-x.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-heatmap/heatmap-x.v42.json index dab0251e18a..a30af0d8fe0 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-heatmap/heatmap-x.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-heatmap/heatmap-x.v42.json @@ -303,6 +303,70 @@ ], "title": "Cells heatmap", "type": "heatmap" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 7, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "show": true, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "12.3.0-pre", + "title": "No data", + "type": "heatmap" } ], "refresh": "", diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-status-history/status-history-thresholds-mappings.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-status-history/status-history-thresholds-mappings.v42.json new file mode 100644 index 00000000000..edce1004bb1 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-status-history/status-history-thresholds-mappings.v42.json @@ -0,0 +1,1163 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 15116, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 11, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "default", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default absolute thresholds", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 8, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default percentage thresholds", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 3, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "override thresholds", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 9.9999 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 14.9999 + }, + "type": "range" + }, + { + "options": { + "from": 15, + "result": { + "color": "red", + "index": 2 + }, + "to": 24.9999 + }, + "type": "range" + }, + { + "options": { + "from": 25, + "result": { + "color": "blue", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 6, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.47, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default value mappings", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 9.9999 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 14.9999 + }, + "type": "range" + }, + { + "options": { + "from": 15, + "result": { + "color": "red", + "index": 2 + }, + "to": 24.9999 + }, + "type": "range" + }, + { + "options": { + "from": 25, + "result": { + "color": "blue", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 7, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "override value mappings", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 10\n },\n {\n \"color\": \"red\",\n \"value\": 15\n },\n {\n \"color\": \"#6ED0E0\",\n \"value\": 25\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000\n ],\n [\n 5,\n 10,\n 20,\n 30\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "field thresholds from data", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 9, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "hide": false, + "max": 30, + "min": 0.01, + "noise": 30, + "refId": "B", + "scenarioId": "random_walk", + "startValue": 1 + } + ], + "title": "threshold from random walk", + "transformations": [ + { + "id": "configFromData", + "options": { + "configRefId": "B", + "mappings": [ + { + "fieldName": "B-series", + "handlerKey": "threshold1" + } + ] + } + } + ], + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "color": "purple", + "index": 0, + "text": "null" + } + }, + "type": "special" + }, + { + "options": { + "match": "nan", + "result": { + "color": "red", + "index": 1, + "text": "NaN" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 18 + }, + "id": 12, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000\n ],\n [\n 5,\n null,\n 20,\n null,\n 40\n ]\n ],\n \"entities\": [null, { \"NaN\": [3]}]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "special null | NaN value mapping from data", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "match": "null+nan", + "result": { + "color": "super-light-red", + "index": 0, + "text": "null + NaN" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 18 + }, + "id": 13, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000\n ],\n [\n 5,\n null,\n 20,\n null,\n 40\n ]\n ],\n \"entities\": [null, { \"NaN\": [1]}]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "special null + NaN value mapping from data", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "match": "false", + "result": { + "color": "red", + "index": 0 + } + }, + "type": "special" + }, + { + "options": { + "match": "null+nan", + "result": { + "color": "blue", + "index": 1, + "text": "null + NaN" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 18 + }, + "id": 14, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000,\n 1674750835000,\n 1674754235000,\n 1674757835000\n ],\n [\n null,\n null,\n false,\n true,\n true,\n false,\n true,\n null\n ]\n ],\n \"entities\": [null, { \"NaN\": [0], \"Undefined\": [1]}]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "boolean values from data", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 18 + }, + "id": 15, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 0 + } + ], + "title": "no data", + "type": "status-history" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [ + "gdev", + "panel-tests", + "state-timeline", + "graph-ng" + ], + "templating": { + "list": [] + }, + "time": { + "from": "2023-01-26T11:29:47.180Z", + "to": "2023-01-26T16:29:39.205Z" + }, + "timepicker": {}, + "timezone": "utc", + "title": "StatusHistory - Thresholds \u0026 Mappings", + "uid": "a2f4ad9e-3b44-4624-8067-35f31be5d309", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-table/table_sparkline_cell.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-table/table_sparkline_cell.v42.json index 3c8310630d7..a0fae31696a 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-table/table_sparkline_cell.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-table/table_sparkline_cell.v42.json @@ -183,6 +183,24 @@ }, "fieldConfig": { "defaults": { + "actions": [ + { + "fetch": { + "body": "{}", + "headers": [ + [ + "Content-Type", + "application/json" + ] + ], + "method": "GET", + "queryParams": [], + "url": "/api/health" + }, + "title": "Get instance health", + "type": "fetch" + } + ], "color": { "fixedColor": "blue", "mode": "shades" @@ -195,6 +213,13 @@ }, "inspect": false }, + "links": [ + { + "targetBlank": true, + "title": "Google Grafana", + "url": "https://google.com/search?q=grafana" + } + ], "mappings": [], "thresholds": { "mode": "absolute", diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-timeline/timeline-thresholds-mappings.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-timeline/timeline-thresholds-mappings.v42.json index f36c1de1710..1d5386a6b17 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-timeline/timeline-thresholds-mappings.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-timeline/timeline-thresholds-mappings.v42.json @@ -1071,32 +1071,27 @@ "type": "state-timeline" }, { - "id": 15, - "type": "state-timeline", - "title": "no data", - "gridPos": { - "x": 18, - "y": 18, - "h": 9, - "w": 6 + "datasource": { + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { - "custom": { - "lineWidth": 0, - "fillOpacity": 70, - "spanNulls": false, - "insertNulls": false, - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "axisPlacement": "auto" - }, "color": { "mode": "palette-classic" }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "fieldMinMax": false, "mappings": [], "thresholds": { "mode": "absolute", @@ -1105,38 +1100,43 @@ "color": "green" } ] - }, - "fieldMinMax": false + } }, "overrides": [] }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 18 + }, + "id": 15, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, "pluginVersion": "12.3.0-pre", "targets": [ { - "scenarioId": "random_walk", "refId": "A", + "scenarioId": "random_walk", "seriesCount": 0 } ], - "datasource": { - "type": "grafana-testdata-datasource" - }, - "options": { - "mergeValues": true, - "showValue": "auto", - "alignValue": "center", - "rowHeight": 0.9, - "legend": { - "showLegend": true, - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none", - "hideZeros": false - } - } + "title": "no data", + "type": "state-timeline" } ], "preload": false, @@ -1160,4 +1160,4 @@ "title": "StateTimeline - Thresholds \u0026 Mappings", "uid": "Kce7z9TVz", "weekStart": "" -} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/input/v11.no-op-migration.json b/apps/dashboard/pkg/migration/testdata/input/v11.no-op-migration.json index 82b054d9d11..7eabaf6bc5d 100644 --- a/apps/dashboard/pkg/migration/testdata/input/v11.no-op-migration.json +++ b/apps/dashboard/pkg/migration/testdata/input/v11.no-op-migration.json @@ -40,13 +40,7 @@ ] }, "annotations": { - "list": [ - { - "name": "Annotations & Alerts", - "datasource": "grafana", - "enable": true - } - ] + "list": [] }, "time": { "from": "now-6h", diff --git a/apps/dashboard/pkg/migration/testdata/input/v15.no-op-migration.json b/apps/dashboard/pkg/migration/testdata/input/v15.no-op-migration.json index e16dca0e87a..df3a62e6dae 100644 --- a/apps/dashboard/pkg/migration/testdata/input/v15.no-op-migration.json +++ b/apps/dashboard/pkg/migration/testdata/input/v15.no-op-migration.json @@ -43,7 +43,10 @@ "list": [ { "name": "Annotations & Alerts", - "datasource": "grafana", + "datasource": { + "type": "grafana", + "uid": "grafana" + }, "enable": true } ] diff --git a/apps/dashboard/pkg/migration/testdata/input/v34.multiple_stats_cloudwatch.json b/apps/dashboard/pkg/migration/testdata/input/v34.multiple_stats_cloudwatch.json index b3d70bd1ac9..fbb88e23546 100644 --- a/apps/dashboard/pkg/migration/testdata/input/v34.multiple_stats_cloudwatch.json +++ b/apps/dashboard/pkg/migration/testdata/input/v34.multiple_stats_cloudwatch.json @@ -110,7 +110,9 @@ "enable": true, "iconColor": "purple", "datasource": { - "uid": "prometheus" + "uid": "default-ds-uid", + "type": "prometheus", + "apiVersion": "v1" } } ] diff --git a/apps/dashboard/pkg/migration/testdata/input/v36.ds_name_to_ref.json b/apps/dashboard/pkg/migration/testdata/input/v36.ds_name_to_ref.json index 0bfb9401d7a..30fb89d4ad0 100644 --- a/apps/dashboard/pkg/migration/testdata/input/v36.ds_name_to_ref.json +++ b/apps/dashboard/pkg/migration/testdata/input/v36.ds_name_to_ref.json @@ -34,6 +34,8 @@ { "name": "Unknown Datasource Annotation - Tests unknown datasource preserved as UID", "datasource": { + "type": "prometheus", + "apiVersion": "v1", "uid": "unknown-datasource-name" } } @@ -119,7 +121,9 @@ { "refId": "B", "datasource": { - "uid": "existing-target-uid" + "uid": "existing-target-uid", + "type": "elasticsearch", + "apiVersion": "v2" } } ] @@ -137,7 +141,8 @@ "refId": "A", "datasource": { "uid": "existing-target-uid", - "type": "elasticsearch" + "type": "elasticsearch", + "apiVersion": "v2" } } ] @@ -167,7 +172,9 @@ { "refId": "A", "datasource": { - "uid": "existing-target-uid" + "uid": "existing-target-uid", + "type": "elasticsearch", + "apiVersion": "v2" } }, { @@ -188,7 +195,9 @@ { "refId": "A", "datasource": { - "uid": "existing-target-uid" + "uid": "existing-target-uid", + "type": "elasticsearch", + "apiVersion": "v2" } } ] @@ -257,7 +266,9 @@ { "refId": "A", "datasource": { - "uid": "existing-target-uid" + "uid": "existing-target-uid", + "type": "elasticsearch", + "apiVersion": "v2" } } ] diff --git a/apps/dashboard/pkg/migration/testdata/input/v42.harky_must.json b/apps/dashboard/pkg/migration/testdata/input/v42.harky_must.json new file mode 100644 index 00000000000..440790150dc --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v42.harky_must.json @@ -0,0 +1,31 @@ +{ + "title": "v42 Migration Test - Harky Must", + "schemaVersion": 41, + "panels": [ + { + "id": 1, + "title": "Panel with hideFrom.viz = true", + "type": "timeseries", + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "viz": true + } + } + ] + } + ] + } + } + ] + } + \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/input/v42.hidefrom_tooltip.json b/apps/dashboard/pkg/migration/testdata/input/v42.hidefrom_tooltip.json index cb7f1ed0027..428b6792464 100644 --- a/apps/dashboard/pkg/migration/testdata/input/v42.hidefrom_tooltip.json +++ b/apps/dashboard/pkg/migration/testdata/input/v42.hidefrom_tooltip.json @@ -49,14 +49,22 @@ ] }, { + "__systemRef": "hideSeriesFrom", "matcher": { - "id": "__systemRef", - "options": "hiddenSeries" + "id": "byNames", + "options": { + "mode": "exclude", + "names": ["foo"], + "prefix": "All except:", + "readOnly": true + } }, "properties": [ { "id": "custom.hideFrom", "value": { + "legend": false, + "tooltip": false, "viz": true } } @@ -103,6 +111,10 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byName", + "options": "Time" + }, "properties": [ { "id": "unit", @@ -123,13 +135,18 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsZero", + "value": 0 + } + }, "properties": [ { - "id": "custom.hideFrom", - "value": { - "viz": false, - "tooltip": false - } + "id": "unit", + "value": "short" } ] } @@ -144,12 +161,17 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byName", + "options": "{__name__=\"ALERTS\", alertname=\"k6CloudServiceErrorsLogged\"}" + }, "properties": [ { "id": "custom.hideFrom", "value": { - "viz": true, - "tooltip": false + "legend": false, + "tooltip": false, + "viz": true } } ] diff --git a/apps/dashboard/pkg/migration/testdata/input/v6.pulldowns_and_templating.json b/apps/dashboard/pkg/migration/testdata/input/v6.pulldowns_and_templating.json index 73c1db88803..91b7af7d38e 100644 --- a/apps/dashboard/pkg/migration/testdata/input/v6.pulldowns_and_templating.json +++ b/apps/dashboard/pkg/migration/testdata/input/v6.pulldowns_and_templating.json @@ -12,14 +12,20 @@ "annotations": [ { "name": "deployment", - "datasource": "prometheus", + "datasource": { + "uid": "default-ds-uid", + "type": "prometheus" + }, "enable": true, "iconColor": "red", "query": "ALERTS{alertname=\"DeploymentStarted\"}" }, { "name": "alerts", - "datasource": "loki", + "datasource": { + "uid": "loki-uid", + "type": "loki" + }, "enable": false, "iconColor": "yellow", "query": "{job=\"alertmanager\"}" @@ -32,12 +38,14 @@ { "name": "environment", "type": "filter", - "allFormat": null + "allFormat": null, + "query": "label_values(up, instance)" }, { "name": "service", "datasource": "prometheus", - "allFormat": "glob" + "allFormat": "glob", + "query": "label_values(up, instance)" }, { "name": "region", diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v11.no-op-migration.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v11.no-op-migration.v42.json index cde0f37b8a3..656752028b4 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v11.no-op-migration.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v11.no-op-migration.v42.json @@ -12,13 +12,6 @@ "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations \u0026 Alerts", "type": "dashboard" - }, - { - "datasource": { - "uid": "grafana" - }, - "enable": true, - "name": "Annotations \u0026 Alerts" } ] }, diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json index 1cda544d3fd..e9eaff02aa7 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json @@ -15,6 +15,7 @@ }, { "datasource": { + "type": "grafana", "uid": "grafana" }, "enable": true, diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.grid_layout_upgrade.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.grid_layout_upgrade.v42.json index e9d3b12001d..81f12085db0 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.grid_layout_upgrade.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v16.grid_layout_upgrade.v42.json @@ -128,6 +128,7 @@ "type": "table" }, { + "autoMigrateFrom": "graph", "gridPos": { "h": 6, "w": 12, @@ -137,9 +138,10 @@ "height": 200, "id": 4, "title": "Network I/O", - "type": "graph" + "type": "timeseries" }, { + "autoMigrateFrom": "graph", "gridPos": { "h": 6, "w": 12, @@ -149,7 +151,7 @@ "height": 200, "id": 5, "title": "Disk I/O", - "type": "graph" + "type": "timeseries" } ], "targets": [ diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v27.repeated_panels_and_constant_variable.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v27.repeated_panels_and_constant_variable.v42.json index 640e4c32e57..5cae702fdad 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v27.repeated_panels_and_constant_variable.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v27.repeated_panels_and_constant_variable.v42.json @@ -50,9 +50,10 @@ "id": 4, "panels": [ { + "autoMigrateFrom": "graph", "id": 6, "title": "Normal nested panel", - "type": "graph" + "type": "timeseries" } ], "targets": [ diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v34.multiple_stats_cloudwatch.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v34.multiple_stats_cloudwatch.v42.json index 12572e9d9b9..5c4596442f6 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v34.multiple_stats_cloudwatch.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v34.multiple_stats_cloudwatch.v42.json @@ -115,7 +115,9 @@ }, { "datasource": { - "uid": "prometheus" + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" }, "enable": true, "iconColor": "purple", diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v36.ds_name_to_ref.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v36.ds_name_to_ref.v42.json index 30eb032bb19..6b68231755f 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v36.ds_name_to_ref.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v36.ds_name_to_ref.v42.json @@ -47,6 +47,8 @@ }, { "datasource": { + "apiVersion": "v1", + "type": "prometheus", "uid": "unknown-datasource-name" }, "name": "Unknown Datasource Annotation - Tests unknown datasource preserved as UID" @@ -135,6 +137,8 @@ }, { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "refId": "B" @@ -152,6 +156,7 @@ "targets": [ { "datasource": { + "apiVersion": "v2", "type": "elasticsearch", "uid": "existing-target-uid" }, @@ -178,6 +183,8 @@ }, { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "description": "Tests panel with expression query - should not inherit expression as panel datasource", @@ -185,6 +192,8 @@ "targets": [ { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "refId": "A" @@ -201,6 +210,8 @@ }, { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "description": "Tests panel inheriting datasource from target when panel datasource was default", @@ -208,6 +219,8 @@ "targets": [ { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "refId": "A" @@ -290,6 +303,8 @@ "panels": [ { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "description": "Nested panel in collapsed row with default datasource", @@ -297,6 +312,8 @@ "targets": [ { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "refId": "A" diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v37.legend_normalization.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v37.legend_normalization.v42.json index 4e0e262a1c1..00db730ff0f 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v37.legend_normalization.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v37.legend_normalization.v42.json @@ -117,6 +117,7 @@ "type": "timeseries" }, { + "autoMigrateFrom": "graph", "id": 12, "options": { "legend": { @@ -125,7 +126,7 @@ } }, "title": "Nested Panel with Hidden DisplayMode", - "type": "graph" + "type": "timeseries" }, { "id": 13, diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v42.harky_must.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v42.harky_must.v42.json new file mode 100644 index 00000000000..7bf27227b56 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v42.harky_must.v42.json @@ -0,0 +1,62 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "id": 1, + "title": "Panel with hideFrom.viz = true", + "type": "timeseries" + } + ], + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "v42 Migration Test - Harky Must", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v42.hidefrom_tooltip.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v42.hidefrom_tooltip.v42.json index 8ef37e2b890..1da5978b1a7 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v42.hidefrom_tooltip.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v42.hidefrom_tooltip.v42.json @@ -67,14 +67,23 @@ ] }, { + "__systemRef": "hideSeriesFrom", "matcher": { - "id": "__systemRef", - "options": "hiddenSeries" + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "foo" + ], + "prefix": "All except:", + "readOnly": true + } }, "properties": [ { "id": "custom.hideFrom", "value": { + "legend": false, "tooltip": true, "viz": true } @@ -121,6 +130,10 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byName", + "options": "Time" + }, "properties": [ { "id": "unit", @@ -143,13 +156,18 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsZero", + "value": 0 + } + }, "properties": [ { - "id": "custom.hideFrom", - "value": { - "tooltip": false, - "viz": false - } + "id": "unit", + "value": "short" } ] } @@ -164,10 +182,15 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byName", + "options": "{__name__=\"ALERTS\", alertname=\"k6CloudServiceErrorsLogged\"}" + }, "properties": [ { "id": "custom.hideFrom", "value": { + "legend": false, "tooltip": true, "viz": true } diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v6.pulldowns_and_templating.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v6.pulldowns_and_templating.v42.json index 1a18f9b01c2..ed1c43eafa9 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v6.pulldowns_and_templating.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v6.pulldowns_and_templating.v42.json @@ -15,7 +15,8 @@ }, { "datasource": { - "uid": "prometheus" + "type": "prometheus", + "uid": "default-ds-uid" }, "enable": true, "iconColor": "red", @@ -24,7 +25,8 @@ }, { "datasource": { - "uid": "loki" + "type": "loki", + "uid": "loki-uid" }, "enable": false, "iconColor": "yellow", @@ -108,6 +110,7 @@ }, "name": "environment", "options": [], + "query": "label_values(up, instance)", "refresh": 1, "type": "query" }, @@ -116,6 +119,7 @@ "datasource": "prometheus", "name": "service", "options": [], + "query": "label_values(up, instance)", "refresh": 1, "type": "query" }, diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v11.no-op-migration.v11.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v11.no-op-migration.v11.json index 7e6680140e0..74389483d79 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v11.no-op-migration.v11.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v11.no-op-migration.v11.json @@ -12,11 +12,6 @@ "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations \u0026 Alerts", "type": "dashboard" - }, - { - "datasource": "grafana", - "enable": true, - "name": "Annotations \u0026 Alerts" } ] }, diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v15.no-op-migration.v15.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v15.no-op-migration.v15.json index 39c7811cf90..d66d7d9aed4 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v15.no-op-migration.v15.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v15.no-op-migration.v15.json @@ -14,7 +14,10 @@ "type": "dashboard" }, { - "datasource": "grafana", + "datasource": { + "type": "grafana", + "uid": "grafana" + }, "enable": true, "name": "Annotations \u0026 Alerts" } diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v16.grid_layout_upgrade.v16.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v16.grid_layout_upgrade.v16.json index f5d6facd9b1..ec62492fb6c 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v16.grid_layout_upgrade.v16.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v16.grid_layout_upgrade.v16.json @@ -78,6 +78,7 @@ "type": "table" }, { + "autoMigrateFrom": "graph", "gridPos": { "h": 6, "w": 12, @@ -87,9 +88,10 @@ "height": 200, "id": 4, "title": "Network I/O", - "type": "graph" + "type": "timeseries" }, { + "autoMigrateFrom": "graph", "gridPos": { "h": 6, "w": 12, @@ -99,7 +101,7 @@ "height": 200, "id": 5, "title": "Disk I/O", - "type": "graph" + "type": "timeseries" } ], "title": "Collapsed Row", diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v27.repeated_panels_and_constant_variable.v27.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v27.repeated_panels_and_constant_variable.v27.json index 40cde403989..9a93c6b3dbf 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v27.repeated_panels_and_constant_variable.v27.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v27.repeated_panels_and_constant_variable.v27.json @@ -30,9 +30,10 @@ "id": 4, "panels": [ { + "autoMigrateFrom": "graph", "id": 6, "title": "Normal nested panel", - "type": "graph" + "type": "timeseries" } ], "title": "Row with repeated panels", diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v34.multiple_stats_cloudwatch.v34.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v34.multiple_stats_cloudwatch.v34.json index 85a9932902b..9da77468be1 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v34.multiple_stats_cloudwatch.v34.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v34.multiple_stats_cloudwatch.v34.json @@ -115,7 +115,9 @@ }, { "datasource": { - "uid": "prometheus" + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" }, "enable": true, "iconColor": "purple", diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v36.ds_name_to_ref.v36.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v36.ds_name_to_ref.v36.json index 3e5f592670f..26d7de2f594 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v36.ds_name_to_ref.v36.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v36.ds_name_to_ref.v36.json @@ -47,6 +47,8 @@ }, { "datasource": { + "apiVersion": "v1", + "type": "prometheus", "uid": "unknown-datasource-name" }, "name": "Unknown Datasource Annotation - Tests unknown datasource preserved as UID" @@ -135,6 +137,8 @@ }, { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "refId": "B" @@ -152,6 +156,7 @@ "targets": [ { "datasource": { + "apiVersion": "v2", "type": "elasticsearch", "uid": "existing-target-uid" }, @@ -178,6 +183,8 @@ }, { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "description": "Tests panel with expression query - should not inherit expression as panel datasource", @@ -185,6 +192,8 @@ "targets": [ { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "refId": "A" @@ -201,6 +210,8 @@ }, { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "description": "Tests panel inheriting datasource from target when panel datasource was default", @@ -208,6 +219,8 @@ "targets": [ { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "refId": "A" @@ -290,6 +303,8 @@ "panels": [ { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "description": "Nested panel in collapsed row with default datasource", @@ -297,6 +312,8 @@ "targets": [ { "datasource": { + "apiVersion": "v2", + "type": "elasticsearch", "uid": "existing-target-uid" }, "refId": "A" diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v37.legend_normalization.v37.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v37.legend_normalization.v37.json index 84b41cc30b9..47b5329ee26 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v37.legend_normalization.v37.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v37.legend_normalization.v37.json @@ -117,6 +117,7 @@ "type": "timeseries" }, { + "autoMigrateFrom": "graph", "id": 12, "options": { "legend": { @@ -125,7 +126,7 @@ } }, "title": "Nested Panel with Hidden DisplayMode", - "type": "graph" + "type": "timeseries" }, { "id": 13, diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v42.harky_must.v42.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v42.harky_must.v42.json new file mode 100644 index 00000000000..7bf27227b56 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v42.harky_must.v42.json @@ -0,0 +1,62 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "fieldConfig": { + "defaults": {}, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Field1" + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "id": 1, + "title": "Panel with hideFrom.viz = true", + "type": "timeseries" + } + ], + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "v42 Migration Test - Harky Must", + "weekStart": "" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v42.hidefrom_tooltip.v42.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v42.hidefrom_tooltip.v42.json index 8ef37e2b890..1da5978b1a7 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v42.hidefrom_tooltip.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v42.hidefrom_tooltip.v42.json @@ -67,14 +67,23 @@ ] }, { + "__systemRef": "hideSeriesFrom", "matcher": { - "id": "__systemRef", - "options": "hiddenSeries" + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "foo" + ], + "prefix": "All except:", + "readOnly": true + } }, "properties": [ { "id": "custom.hideFrom", "value": { + "legend": false, "tooltip": true, "viz": true } @@ -121,6 +130,10 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byName", + "options": "Time" + }, "properties": [ { "id": "unit", @@ -143,13 +156,18 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byValue", + "options": { + "op": "gte", + "reducer": "allIsZero", + "value": 0 + } + }, "properties": [ { - "id": "custom.hideFrom", - "value": { - "tooltip": false, - "viz": false - } + "id": "unit", + "value": "short" } ] } @@ -164,10 +182,15 @@ "defaults": {}, "overrides": [ { + "matcher": { + "id": "byName", + "options": "{__name__=\"ALERTS\", alertname=\"k6CloudServiceErrorsLogged\"}" + }, "properties": [ { "id": "custom.hideFrom", "value": { + "legend": false, "tooltip": true, "viz": true } diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v6.pulldowns_and_templating.v6.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v6.pulldowns_and_templating.v6.json index 8cdf9a36a40..509bdb7a181 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v6.pulldowns_and_templating.v6.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v6.pulldowns_and_templating.v6.json @@ -14,14 +14,20 @@ "type": "dashboard" }, { - "datasource": "prometheus", + "datasource": { + "type": "prometheus", + "uid": "default-ds-uid" + }, "enable": true, "iconColor": "red", "name": "deployment", "query": "ALERTS{alertname=\"DeploymentStarted\"}" }, { - "datasource": "loki", + "datasource": { + "type": "loki", + "uid": "loki-uid" + }, "enable": false, "iconColor": "yellow", "name": "alerts", @@ -79,6 +85,7 @@ { "name": "environment", "options": [], + "query": "label_values(up, instance)", "type": "query" }, { @@ -86,6 +93,7 @@ "datasource": "prometheus", "name": "service", "options": [], + "query": "label_values(up, instance)", "type": "query" }, { diff --git a/apps/dashboard/pkg/migration/testutil/mocks.go b/apps/dashboard/pkg/migration/testutil/mocks.go index 1c70c397029..dbc7df72728 100644 --- a/apps/dashboard/pkg/migration/testutil/mocks.go +++ b/apps/dashboard/pkg/migration/testutil/mocks.go @@ -88,6 +88,30 @@ func (p *ConfigurableDataSourceProvider) getStandardTestDataSources() []schemave Name: "-- Mixed --", ID: 6, }, + { + Default: false, + UID: "influx-uid", + Type: "influxdb", + APIVersion: "v1", + Name: "InfluxDB Test Datasource", + ID: 7, + }, + { + Default: false, + UID: "cloudwatch-uid", + Type: "cloudwatch", + APIVersion: "v1", + Name: "CloudWatch Test Datasource", + ID: 8, + }, + { + Default: false, + UID: "-- Grafana --", + Type: "grafana", + APIVersion: "v1", + Name: "-- Grafana --", + ID: 9, + }, } } diff --git a/apps/dashboard/pkg/migration/testutil/mocks_test.go b/apps/dashboard/pkg/migration/testutil/mocks_test.go index 5d27074f79e..dfbd0e6b86e 100644 --- a/apps/dashboard/pkg/migration/testutil/mocks_test.go +++ b/apps/dashboard/pkg/migration/testutil/mocks_test.go @@ -15,7 +15,7 @@ func TestConfigurableDataSourceProvider(t *testing.T) { provider := NewDataSourceProvider(StandardTestConfig) dataSources := provider.GetDataSourceInfo(ctx) - require.Len(t, dataSources, 6) + require.Len(t, dataSources, 9) // Verify standard test configuration defaultDS := dataSources[0] @@ -58,8 +58,8 @@ func TestConfigurableDataSourceProvider(t *testing.T) { devDS1 := devProvider1.GetDataSourceInfo(ctx) devDS2 := devProvider2.GetDataSourceInfo(ctx) - require.Len(t, standardDS1, 6) - require.Len(t, standardDS2, 6) + require.Len(t, standardDS1, 9) + require.Len(t, standardDS2, 9) require.Len(t, devDS1, 6) require.Len(t, devDS2, 6) @@ -74,7 +74,7 @@ func TestConfigurableDataSourceProvider(t *testing.T) { provider := NewDataSourceProvider("unknown-config") dataSources := provider.GetDataSourceInfo(ctx) - require.Len(t, dataSources, 6) + require.Len(t, dataSources, 9) // Should default to standard configuration defaultDS := dataSources[0] diff --git a/apps/example/Makefile b/apps/example/Makefile new file mode 100644 index 00000000000..230bfd4149a --- /dev/null +++ b/apps/example/Makefile @@ -0,0 +1,9 @@ +include ../sdk.mk + +.PHONY: generate # Run Grafana App SDK code generation +generate: install-app-sdk update-app-sdk + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --defencoding=none \ No newline at end of file diff --git a/apps/example/README.md b/apps/example/README.md new file mode 100644 index 00000000000..8a69389c3fa --- /dev/null +++ b/apps/example/README.md @@ -0,0 +1,120 @@ +# Example App + +This App is an example of general app capabilities when developing on the grafana app platform. + +## Enabling the App + +By default, the example app is disabled. To enable this App, add the following to your `conf/custom.ini`: + +``` +[grafana-apiserver] +runtime_config = example.grafana.app/v0alpha1=true,example.grafana.app/v1alpha1=true +``` + +## Manifest + +The source of the app's schemas and list of capabilities is the manifest, which is generated from [kinds/manifest.cue](./kinds/manifest.cue). +The `Example` kind is defined for [v0alpha1 here](./kinds/example_v0alpha1) and [v1alpha1 (default) here](./kinds/example_v1alpha1.cue). +The root definition of the `Example` kind that both versions share is defined [here](./kinds/example.cue). + +The CUE is used to generate code (and the AppManifest) when `make generate` is run. + +## Code + +All of the app's code is located in [pkg/app](./pkg/app/). The [New() function](./pkg/app/app.go#20) in `pkg/app/app.go` is the entry point of the app, +and everything should be discoverable from there. + +The code to register the app with the grafana API server (including inserting the app-specific config [ExampleConfig](./pkg/app/config.go)) +is located in [/pkg/registry/apps/example/register.go](../../pkg/registry/apps/example/register.go). + +Any app must also have its installer listed in [WireSet](../../pkg/registry/apps/wireset.go) and added to `installers` in [ProvideAppInstallers](../../pkg/registry/apps/apps.go). When building a new app, make to to regerate wire (`make build` in the root of the repo does this). + +### Generated Code + +The [pkg/apis](./pkg/apis/) package, and all its subdirectories, contain code generated by `make generate`. +This code should not be edited, but it can be useful to look at when working through the flow of the app. + +## Sample Swagger Payloads + +Navigate to [localhost:3000/swagger?api=example.grafana.app-v1alpha1](http://localhost:3000/swagger?api=example.grafana.app-v1alpha1) to view the swagger for the app's `v1alpha1` version +(this version has the most capabilities/endpoints). You can use the `Execute` button to make requests via the swagger UI. + +Create a new `Example` resource with via swagger with: + +```json +{ + "apiVersion": "example.grafana.app/v1alpha1", + "kind": "Example", + "metadata": { + "name": "test", + "namespace": "default" + }, + "spec": { + "firstField": "test", + "secondField": 0, + "list": { + "info": "foo", + "next": { + "info": "bar" + } + } + } +} +``` + +Create an invalid object which will be rejected by validation: + +```json +{ + "apiVersion": "example.grafana.app/v1alpha1", + "kind": "Example", + "metadata": { + "name": "invalid", + "namespace": "default" + }, + "spec": { + "firstField": "test", + "secondField": 0, + "list": { + "info": "foo", + "next": { + "info": "bar" + } + } + } +} +``` + +Update `custom` subresource: + +```json +{ + "apiVersion": "example.grafana.app/v1alpha1", + "kind": "Example", + "metadata": { + "namespace": "default", + "name": "test", + "resourceVersion": "" + }, + "custom": { + "myField": "foo", + "otherField": "bar" + } +} +``` + +(`metadata.resourceVersion` is required for an update, use the value you get from a GET request) + +## cURL + +You can also interact with the grafana API server via a kubeconfig set up for it, or via `curl` using the `-u :` flag. +Currently, cluster-scoped custom routes are erased from the swagger as part of grafana's APIServer code, but can still be called via `curl`, like so: + +```bash +curl -u admin:admin http://localhost:3000/apis/example.grafana.app/v1alpha1/other +``` + +``` +% curl -u admin:admin http://localhost:3000/apis/example.grafana.app/v1alpha1/other +{"message":"This is a cluster route"} +``` diff --git a/apps/example/go.mod b/apps/example/go.mod new file mode 100644 index 00000000000..2a3e75d75c0 --- /dev/null +++ b/apps/example/go.mod @@ -0,0 +1,100 @@ +module github.com/grafana/grafana/apps/example + +go 1.25.3 + +require ( + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe + k8s.io/apimachinery v0.34.1 + k8s.io/apiserver v0.34.1 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect + github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect + github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect + github.com/onsi/gomega v1.36.2 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/apps/example/go.sum b/apps/example/go.sum new file mode 100644 index 00000000000..fcab8eb625f --- /dev/null +++ b/apps/example/go.sum @@ -0,0 +1,256 @@ +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/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= +github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= +github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= +github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= +github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe h1:pPoFj2bQKDBg5EyEdOU+Jn+0hQN+M775Qihk73RbdSs= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe/go.mod h1:zn/yoxKpWA2KUsxOhQbSbL8OCkF2JNLpSEHs+hQYvdM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/example/kinds/cue.mod/module.cue b/apps/example/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..b2f49fc6dc6 --- /dev/null +++ b/apps/example/kinds/cue.mod/module.cue @@ -0,0 +1,2 @@ +module: "github.com/grafana/grafana/apps/example/kinds" +language: version: "v0.8.2" diff --git a/apps/example/kinds/example.cue b/apps/example/kinds/example.cue new file mode 100644 index 00000000000..12e7f5a496d --- /dev/null +++ b/apps/example/kinds/example.cue @@ -0,0 +1,52 @@ +package kinds + +// This is our ExampleKind definition, which contains kind metadata. It is the same across all versions of the kind. +exampleKind: { + // Name is the human-readable name which is used for generated type names. + kind: "Example" + // Scope determines the scope of the kind in the API server. It currently allows two values: + // * Namespaced - resources for this kind are created inside namespaces + // * Cluster - resource for this kind are always cluster-wide (this can be thought of as a "global" namespace) + // If not present, this defaults to "Namespaced" + scope: "Namespaced" + // [OPTIONAL] + // The human-readable plural form of the "name" field. + // Will default to +"s" if not present. + pluralName: "Examples" + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } + mutation: { + operations: [ + "CREATE", + "UPDATE", + ] + } + conversion: true + // [OPTIONAL] + // Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind. + // If not present, default values within the codegen trait are used. + // If you wish to specify codegen per-version, put this section in the version's object + // (for example, exampleKindv1alpha1) instead. + codegen: { + // [OPTIONAL] + // ts contains TypeScript code generation properties for the kind + ts: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate front-end TypeScript code for the kind. + // Defaults to true if not present. + enabled: true + } + // [OPTIONAL] + // go contains go code generation properties for the kind + go: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate back-end go code for the kind. + // Defaults to true if not present. + enabled: true + } + } +} diff --git a/apps/example/kinds/example_v0alpha1.cue b/apps/example/kinds/example_v0alpha1.cue new file mode 100644 index 00000000000..645189f7dc2 --- /dev/null +++ b/apps/example/kinds/example_v0alpha1.cue @@ -0,0 +1,15 @@ +package kinds + +// This is the v0alpha1 version of the kind. Please see v1alpha1 for more complete comments +// and a more complex schema and set of capabilities. +examplev0alpha1: exampleKind & { + schema: { + // Spec is the schema of our resource. The spec should include all the user-editable information for the kind. + spec: { + firstField: int + } + status: { + lastObservedGeneration: int64 + } + } +} \ No newline at end of file diff --git a/apps/example/kinds/example_v1alpha1.cue b/apps/example/kinds/example_v1alpha1.cue new file mode 100644 index 00000000000..bb54c59d56c --- /dev/null +++ b/apps/example/kinds/example_v1alpha1.cue @@ -0,0 +1,76 @@ +package kinds + +// This is the v1alpha1 version of the kind, which joins the kind metadata and +// version-specific information for the kind, such as the schema +examplev1alpha1: exampleKind & { + // schema is the schema for this version of the kind + // As an API server-expressable resource, the schema has a restricted format: + // { + // spec: { ... } + // status: { ... } // optional + // metadata: { ... } // optional + // } + // `spec` must always be present, and is the schema for the object. + // `status` is optional, and should contain status or state information which is typically not user-editable + // (controlled by controllers/operators). The kind system adds some implicit status information which is + // common across all kinds, and becomes present in the unified lineage used for code generation and other tooling. + // `metadata` is optional, and should contain kind- or schema-specific metadata. The kind system adds + // an explicit set of common metadata which can be found in the definition file for a CUE kind at + // [https://github.com/grafana/grafana-app-sdk/blob/main/codegen/cuekind/def.cue] + // additional metadata fields cannot conflict with the common metadata field names + schema: { + // #DefinedType is a re-usable definition for us to use in our schema. + // Fields leading with # are definitions in CUE and won't be included in the generated types. + #DefinedType: { + // Info is information about this entry. This comment, like all comments + // on fields or definitions, will be copied into the generated types as well. + info: string + // Next is an optional next element in the DefinedType, allowing for a self-referential + // linked-list like structure. The ? in the field makes this optional. + next?: #DefinedType + } + // Spec is the schema of our resource. The spec should include all the user-editable information for the kind. + spec: { + // Example fields + firstField: string + secondField: int + list?: #DefinedType + } + // status is where state and status information which may be used or updated by the operator or back-end should be placed + // If you do not have any such information, you do not need to include this field, + // however, as mentioned above, certain fields will be added by the kind system regardless. + status: { + lastObservedGeneration: int64 + } + // Custom is a subresource that will be stored the same way status is stored, + // and requires using the /custom route to update. + // Its content is returned as part of a GET to the resource itself, just like with status. + // To route a subresource to an arbitrary handler, use the 'routes' field instead (see below). + custom: { + myField: string + otherField: string + } + // metadata if where kind- and schema-specific metadata goes. This is converted into typed annotations + // with getters and setters by the code generation. + //metadata: { + // kindSpecificField: string + //} + } + + // routes contains subresource routes for the kind, which are exposed as HTTP handlers on 'examples//'. + // This allows you to add additional non-storage-based handlers to your kind. + // These should only be used if the behavior cannot be accomplished by reconciliation on storage events. + routes: { + // This will add a handler for /foo on the resource + "foo": { + // GET request handler. A subresource route can have multiple methods attached to it. + // Allowed values are GET, POST, PUT, DELETE, PATCH, HEAD, and OPTIONS + "GET": { + // The response type for the GET /foo method. This will generate a go type, and will also be used for the OpenAPI definition for the route. + response: { + message: string + } + } + } + } +} \ No newline at end of file diff --git a/apps/example/kinds/manifest.cue b/apps/example/kinds/manifest.cue new file mode 100644 index 00000000000..934d419623d --- /dev/null +++ b/apps/example/kinds/manifest.cue @@ -0,0 +1,116 @@ +package kinds + +manifest: { + // appName is the unique name of your app. It is used to reference the app from other config objects, + // and to generate the group used by your app in the app platform API. + appName: "example" + // groupOverride can be used to specify a non-appName-based API group. + // By default, an app's API group is LOWER(REPLACE(appName, '-', '')).ext.grafana.com, + // but there are cases where this needs to be changed. + // Keep in mind that changing this after an app is deployed can cause problems with clients and/or kind data. + groupOverride: "example.grafana.app" + + // versions is a map of versions supported by your app. Version names should follow the format "v" or + // "v(alpha|beta)". Each version contains the kinds your app manages for that version. + // If your app needs access to kinds managed by another app, use permissions.accessKinds to allow your app access. + versions: { + "v0alpha1": v0alpha1 + "v1alpha1": v1alpha1 + } + // extraPermissions contains any additional permissions your app may require to function. + // Your app will always have all permissions for each kind it manages (the items defined in 'kinds'). + extraPermissions: { + // If your app needs access to additional kinds supplied by other apps, you can list them here + accessKinds: [ + // Here is an example for your app accessing the playlist kind for reads and watch + // { + // group: "playlist.grafana.app" + // resource: "playlists" + // actions: ["get","list","watch"] + // } + ] + } +} + +v0alpha1: { + kinds: [examplev0alpha1] + // This is explicitly set to false to keep the example app disabled by default. + // It can be enabled via conf overrides, or by setting this value to true and regenerating. + served: false +} + +// v1alpha1 is the v1alpha1 version of the app's API. +// It includes kinds which the v1alpha1 API serves, and (future) custom routes served globally from the v1alpha1 version. +v1alpha1: { + // kinds is the list of kinds served by this version + kinds:[examplev1alpha1] + // [OPTIONAL] + // served indicates whether this particular version is served by the API server. + // served should be set to false before a version is removed from the manifest entirely. + // served defaults to true if not present. + // This is explicitly set to false to keep the example app disabled by default. + // It can be enabled via conf overrides, or by setting this value to true and regenerating. + served: false + // routes contains resource routes for the version, which are split into 'namespaced' and 'cluster' scoped routes. + // This allows you to add additional non-storage- and non-kind- based handlers for your app. + // These should only be used if the behavior cannot be accomplished by reconciliation on storage events or subresource routes on a kind. + routes: { + // namespaced contains namespace-scoped resource routes for the version, + // which are exposed as HTTP handlers on '/namespaces//'. + namespaced: { + "/something": { + "GET": { + response: { + namespace: string + message: string + } + request: { + query: { + message?: string + } + } + } + } + } + // cluster contains cluster-scoped resource routes for the version, + // which are exposed as HTTP handlers on '/'. + cluster: { + "/other": { + "GET": { + response: { + message: string + } + request: { + query: { + message?: string + } + } + responseMetadata: typeMeta: false // Don't generate or return kubernetes type metadata for this object + } + } + } + } + // [OPTIONAL] + // Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind. + // If not present, default values within the codegen trait are used. + // If you wish to specify codegen per-version, put this section in the version's object + // (for example, v1alpha1) instead. + codegen: { + // [OPTIONAL] + // ts contains TypeScript code generation properties for the kind + ts: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate front-end TypeScript code for the kind. + // Defaults to true if not present. + enabled: true + } + // [OPTIONAL] + // go contains go code generation properties for the kind + go: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate back-end go code for the kind. + // Defaults to true if not present. + enabled: true + } + } +} \ No newline at end of file diff --git a/apps/example/pkg/apis/example/v0alpha1/constants.go b/apps/example/pkg/apis/example/v0alpha1/constants.go new file mode 100644 index 00000000000..edbbe6d97c2 --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/constants.go @@ -0,0 +1,18 @@ +package v0alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "example.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v0alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/example/pkg/apis/example/v0alpha1/example_client_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_client_gen.go new file mode 100644 index 00000000000..e528b6b5cbc --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ExampleClient struct { + client *resource.TypedClient[*Example, *ExampleList] +} + +func NewExampleClient(client resource.Client) *ExampleClient { + return &ExampleClient{ + client: resource.NewTypedClient[*Example, *ExampleList](client, ExampleKind()), + } +} + +func NewExampleClientFromGenerator(generator resource.ClientGenerator) (*ExampleClient, error) { + c, err := generator.ClientFor(ExampleKind()) + if err != nil { + return nil, err + } + return NewExampleClient(c), nil +} + +func (c *ExampleClient) Get(ctx context.Context, identifier resource.Identifier) (*Example, error) { + return c.client.Get(ctx, identifier) +} + +func (c *ExampleClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*ExampleList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *ExampleClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*ExampleList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *ExampleClient) Create(ctx context.Context, obj *Example, opts resource.CreateOptions) (*Example, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = ExampleKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *ExampleClient) Update(ctx context.Context, obj *Example, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *ExampleClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Example, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *ExampleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ExampleStatus, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, &Example{ + TypeMeta: metav1.TypeMeta{ + Kind: ExampleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *ExampleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_codec_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_codec_gen.go similarity index 55% rename from apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_codec_gen.go rename to apps/example/pkg/apis/example/v0alpha1/example_codec_gen.go index 48b7e98fd2f..67bb330bfe6 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_codec_gen.go +++ b/apps/example/pkg/apis/example/v0alpha1/example_codec_gen.go @@ -11,18 +11,18 @@ import ( "github.com/grafana/grafana-app-sdk/resource" ) -// PluginInstallJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding -type PluginInstallJSONCodec struct{} +// ExampleJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type ExampleJSONCodec struct{} // Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` -func (*PluginInstallJSONCodec) Read(reader io.Reader, into resource.Object) error { +func (*ExampleJSONCodec) Read(reader io.Reader, into resource.Object) error { return json.NewDecoder(reader).Decode(into) } // Write writes JSON-encoded bytes into `writer` marshaled from `from` -func (*PluginInstallJSONCodec) Write(writer io.Writer, from resource.Object) error { +func (*ExampleJSONCodec) Write(writer io.Writer, from resource.Object) error { return json.NewEncoder(writer).Encode(from) } // Interface compliance checks -var _ resource.Codec = &PluginInstallJSONCodec{} +var _ resource.Codec = &ExampleJSONCodec{} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_metadata_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_metadata_gen.go similarity index 83% rename from apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_metadata_gen.go rename to apps/example/pkg/apis/example/v0alpha1/example_metadata_gen.go index 0e944597301..1e390bb070f 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_metadata_gen.go +++ b/apps/example/pkg/apis/example/v0alpha1/example_metadata_gen.go @@ -9,7 +9,7 @@ import ( // metadata contains embedded CommonMetadata and can be extended with custom string fields // TODO: use CommonMetadata instead of redefining here; currently needs to be defined here // without external reference as using the CommonMetadata reference breaks thema codegen. -type PluginInstallMetadata struct { +type ExampleMetadata struct { UpdateTimestamp time.Time `json:"updateTimestamp"` CreatedBy string `json:"createdBy"` Uid string `json:"uid"` @@ -22,9 +22,9 @@ type PluginInstallMetadata struct { Labels map[string]string `json:"labels"` } -// NewPluginInstallMetadata creates a new PluginInstallMetadata object. -func NewPluginInstallMetadata() *PluginInstallMetadata { - return &PluginInstallMetadata{ +// NewExampleMetadata creates a new ExampleMetadata object. +func NewExampleMetadata() *ExampleMetadata { + return &ExampleMetadata{ Finalizers: []string{}, Labels: map[string]string{}, } diff --git a/apps/example/pkg/apis/example/v0alpha1/example_object_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_object_gen.go new file mode 100644 index 00000000000..9b846649ebd --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Example struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Example + Spec ExampleSpec `json:"spec" yaml:"spec"` + + Status ExampleStatus `json:"status" yaml:"status"` +} + +func (o *Example) GetSpec() any { + return o.Spec +} + +func (o *Example) SetSpec(spec any) error { + cast, ok := spec.(ExampleSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Example) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *Example) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *Example) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(ExampleStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type ExampleStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Example) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Example) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Example) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Example) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Example) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Example) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Example) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Example) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Example) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Example) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Example) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Example) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Example) DeepCopy() *Example { + cpy := &Example{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Example) DeepCopyInto(dst *Example) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &Example{} + +// +k8s:openapi-gen=true +type ExampleList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Example `json:"items" yaml:"items"` +} + +func (o *ExampleList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ExampleList) Copy() resource.ListObject { + cpy := &ExampleList{ + TypeMeta: o.TypeMeta, + Items: make([]Example, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Example); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *ExampleList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *ExampleList) SetItems(items []resource.Object) { + o.Items = make([]Example, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Example) + } +} + +func (o *ExampleList) DeepCopy() *ExampleList { + cpy := &ExampleList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *ExampleList) DeepCopyInto(dst *ExampleList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &ExampleList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *ExampleSpec) DeepCopy() *ExampleSpec { + cpy := &ExampleSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *ExampleSpec) DeepCopyInto(dst *ExampleSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of ExampleStatus +func (s *ExampleStatus) DeepCopy() *ExampleStatus { + cpy := &ExampleStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies ExampleStatus into another ExampleStatus object +func (s *ExampleStatus) DeepCopyInto(dst *ExampleStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/example/pkg/apis/example/v0alpha1/example_schema_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_schema_gen.go new file mode 100644 index 00000000000..1feaa8d42d1 --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaExample = resource.NewSimpleSchema("example.grafana.app", "v0alpha1", &Example{}, &ExampleList{}, resource.WithKind("Example"), + resource.WithPlural("examples"), resource.WithScope(resource.NamespacedScope)) + kindExample = resource.Kind{ + Schema: schemaExample, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &ExampleJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func ExampleKind() resource.Kind { + return kindExample +} + +// Schema returns a resource.SimpleSchema representation of Example +func ExampleSchema() *resource.SimpleSchema { + return schemaExample +} + +// Interface compliance checks +var _ resource.Schema = kindExample diff --git a/apps/example/pkg/apis/example/v0alpha1/example_spec_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_spec_gen.go new file mode 100644 index 00000000000..f0e408554ab --- /dev/null +++ b/apps/example/pkg/apis/example/v0alpha1/example_spec_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +// +k8s:openapi-gen=true +type ExampleSpec struct { + FirstField int64 `json:"firstField"` +} + +// NewExampleSpec creates a new ExampleSpec object. +func NewExampleSpec() *ExampleSpec { + return &ExampleSpec{} +} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_status_gen.go b/apps/example/pkg/apis/example/v0alpha1/example_status_gen.go similarity index 51% rename from apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_status_gen.go rename to apps/example/pkg/apis/example/v0alpha1/example_status_gen.go index 4ee320a974c..75c78737ada 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_status_gen.go +++ b/apps/example/pkg/apis/example/v0alpha1/example_status_gen.go @@ -3,42 +3,43 @@ package v0alpha1 // +k8s:openapi-gen=true -type RoutingTreestatusOperatorState struct { +type ExamplestatusOperatorState struct { // lastEvaluation is the ResourceVersion last evaluated LastEvaluation string `json:"lastEvaluation"` // state describes the state of the lastEvaluation. // It is limited to three possible states for machine evaluation. - State RoutingTreeStatusOperatorStateState `json:"state"` + State ExampleStatusOperatorStateState `json:"state"` // descriptiveState is an optional more descriptive state field which has no requirements on format DescriptiveState *string `json:"descriptiveState,omitempty"` // details contains any extra information that is operator-specific Details map[string]interface{} `json:"details,omitempty"` } -// NewRoutingTreestatusOperatorState creates a new RoutingTreestatusOperatorState object. -func NewRoutingTreestatusOperatorState() *RoutingTreestatusOperatorState { - return &RoutingTreestatusOperatorState{} +// NewExamplestatusOperatorState creates a new ExamplestatusOperatorState object. +func NewExamplestatusOperatorState() *ExamplestatusOperatorState { + return &ExamplestatusOperatorState{} } // +k8s:openapi-gen=true -type RoutingTreeStatus struct { +type ExampleStatus struct { + LastObservedGeneration int64 `json:"lastObservedGeneration"` // operatorStates is a map of operator ID to operator state evaluations. // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]RoutingTreestatusOperatorState `json:"operatorStates,omitempty"` + OperatorStates map[string]ExamplestatusOperatorState `json:"operatorStates,omitempty"` // additionalFields is reserved for future use AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` } -// NewRoutingTreeStatus creates a new RoutingTreeStatus object. -func NewRoutingTreeStatus() *RoutingTreeStatus { - return &RoutingTreeStatus{} +// NewExampleStatus creates a new ExampleStatus object. +func NewExampleStatus() *ExampleStatus { + return &ExampleStatus{} } // +k8s:openapi-gen=true -type RoutingTreeStatusOperatorStateState string +type ExampleStatusOperatorStateState string const ( - RoutingTreeStatusOperatorStateStateSuccess RoutingTreeStatusOperatorStateState = "success" - RoutingTreeStatusOperatorStateStateInProgress RoutingTreeStatusOperatorStateState = "in_progress" - RoutingTreeStatusOperatorStateStateFailed RoutingTreeStatusOperatorStateState = "failed" + ExampleStatusOperatorStateStateSuccess ExampleStatusOperatorStateState = "success" + ExampleStatusOperatorStateStateInProgress ExampleStatusOperatorStateState = "in_progress" + ExampleStatusOperatorStateStateFailed ExampleStatusOperatorStateState = "failed" ) diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/constants.go b/apps/example/pkg/apis/example/v1alpha1/constants.go similarity index 92% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/constants.go rename to apps/example/pkg/apis/example/v1alpha1/constants.go index e6000ff1f42..68a0c69a614 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/constants.go +++ b/apps/example/pkg/apis/example/v1alpha1/constants.go @@ -4,7 +4,7 @@ import "k8s.io/apimachinery/pkg/runtime/schema" const ( // APIGroup is the API group used by all kinds in this package - APIGroup = "shorturl.grafana.app" + APIGroup = "example.grafana.app" // APIVersion is the API version used by all kinds in this package APIVersion = "v1alpha1" ) diff --git a/apps/example/pkg/apis/example/v1alpha1/example_client_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_client_gen.go new file mode 100644 index 00000000000..686c663c12c --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_client_gen.go @@ -0,0 +1,144 @@ +package v1alpha1 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ExampleClient struct { + client *resource.TypedClient[*Example, *ExampleList] +} + +func NewExampleClient(client resource.Client) *ExampleClient { + return &ExampleClient{ + client: resource.NewTypedClient[*Example, *ExampleList](client, ExampleKind()), + } +} + +func NewExampleClientFromGenerator(generator resource.ClientGenerator) (*ExampleClient, error) { + c, err := generator.ClientFor(ExampleKind()) + if err != nil { + return nil, err + } + return NewExampleClient(c), nil +} + +func (c *ExampleClient) Get(ctx context.Context, identifier resource.Identifier) (*Example, error) { + return c.client.Get(ctx, identifier) +} + +func (c *ExampleClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*ExampleList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *ExampleClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*ExampleList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *ExampleClient) Create(ctx context.Context, obj *Example, opts resource.CreateOptions) (*Example, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = ExampleKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *ExampleClient) Update(ctx context.Context, obj *Example, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *ExampleClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Example, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *ExampleClient) UpdateCustom(ctx context.Context, identifier resource.Identifier, newCustom ExampleCustom, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, &Example{ + TypeMeta: metav1.TypeMeta{ + Kind: ExampleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Custom: newCustom, + }, resource.UpdateOptions{ + Subresource: "custom", + ResourceVersion: opts.ResourceVersion, + }) +} +func (c *ExampleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ExampleStatus, opts resource.UpdateOptions) (*Example, error) { + return c.client.Update(ctx, &Example{ + TypeMeta: metav1.TypeMeta{ + Kind: ExampleKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *ExampleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} + +type GetFooRequest struct { + Params GetFooRequestParams + Headers http.Header +} + +func (c *ExampleClient) GetFoo(ctx context.Context, identifier resource.Identifier, request GetFooRequest) (*GetFoo, error) { + params := url.Values{} + resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{ + Path: "foo", + Verb: "GET", + Query: params, + Headers: request.Headers, + }) + if err != nil { + return nil, err + } + cast := GetFoo{} + err = json.Unmarshal(resp, &cast) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal response bytes into GetFoo: %w", err) + } + return &cast, nil +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_codec_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_codec_gen.go new file mode 100644 index 00000000000..80cee2b1cfc --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// ExampleJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type ExampleJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*ExampleJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*ExampleJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &ExampleJSONCodec{} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_custom_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_custom_gen.go new file mode 100644 index 00000000000..bcb4f4bb363 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_custom_gen.go @@ -0,0 +1,25 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// Custom is a subresource that will be stored the same way status is stored, +// and requires using the /custom route to update. +// Its content is returned as part of a GET to the resource itself, just like with status. +// To route a subresource to an arbitrary handler, use the 'routes' field instead (see below). +// metadata if where kind- and schema-specific metadata goes. This is converted into typed annotations +// with getters and setters by the code generation. +// +// metadata: { +// kindSpecificField: string +// } +// +// +k8s:openapi-gen=true +type ExampleCustom struct { + MyField string `json:"myField"` + OtherField string `json:"otherField"` +} + +// NewExampleCustom creates a new ExampleCustom object. +func NewExampleCustom() *ExampleCustom { + return &ExampleCustom{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_body_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_body_types_gen.go new file mode 100644 index 00000000000..42344f1cb6d --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_body_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +type GetFooRequestBody struct { + Bar string `json:"bar"` +} + +// NewGetFooRequestBody creates a new GetFooRequestBody object. +func NewGetFooRequestBody() *GetFooRequestBody { + return &GetFooRequestBody{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_object_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_object_gen.go new file mode 100644 index 00000000000..bf6a51b1815 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetFooRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetFooRequestParams `json:",inline"` +} + +func NewGetFooRequestParamsObject() *GetFooRequestParamsObject { + return &GetFooRequestParamsObject{} +} + +func (o *GetFooRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetFooRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetFooRequestParamsObject) DeepCopyInto(dst *GetFooRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetFooRequestParams := GetFooRequestParams{} + _ = resource.CopyObjectInto(&dstGetFooRequestParams, &o.GetFooRequestParams) +} + +var _ runtime.Object = NewGetFooRequestParamsObject() diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_types_gen.go new file mode 100644 index 00000000000..f0f04d7d0a2 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_request_params_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +type GetFooRequestParams struct { + Message *string `json:"message,omitempty"` +} + +// NewGetFooRequestParams creates a new GetFooRequestParams object. +func NewGetFooRequestParams() *GetFooRequestParams { + return &GetFooRequestParams{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_body_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_body_types_gen.go new file mode 100644 index 00000000000..ab77d0905b1 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_body_types_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// The response type for the GET /foo method. This will generate a go type, and will also be used for the OpenAPI definition for the route. +// +k8s:openapi-gen=true +type GetFooBody struct { + Message string `json:"message"` +} + +// NewGetFooBody creates a new GetFooBody object. +func NewGetFooBody() *GetFooBody { + return &GetFooBody{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_object_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_object_types_gen.go new file mode 100644 index 00000000000..ba41aba79c5 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_getfoo_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type GetFoo struct { + metav1.TypeMeta `json:",inline"` + GetFooBody `json:",inline"` +} + +func NewGetFoo() *GetFoo { + return &GetFoo{} +} + +func (t *GetFooBody) DeepCopyInto(dst *GetFooBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetFoo) DeepCopyObject() runtime.Object { + dst := NewGetFoo() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetFoo) DeepCopyInto(dst *GetFoo) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetFooBody.DeepCopyInto(&dst.GetFooBody) +} + +var _ runtime.Object = NewGetFoo() diff --git a/apps/example/pkg/apis/example/v1alpha1/example_metadata_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_metadata_gen.go new file mode 100644 index 00000000000..e3974409f20 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type ExampleMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewExampleMetadata creates a new ExampleMetadata object. +func NewExampleMetadata() *ExampleMetadata { + return &ExampleMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_object_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_object_gen.go new file mode 100644 index 00000000000..efd1374e855 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_object_gen.go @@ -0,0 +1,347 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Example struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Example + Spec ExampleSpec `json:"spec" yaml:"spec"` + + Status ExampleStatus `json:"status" yaml:"status"` + + Custom ExampleCustom `json:"custom" yaml:"custom"` +} + +func (o *Example) GetSpec() any { + return o.Spec +} + +func (o *Example) SetSpec(spec any) error { + cast, ok := spec.(ExampleSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Example) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + + "custom": o.Custom, + } +} + +func (o *Example) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + + case "custom": + return o.Custom, true + default: + return nil, false + } +} + +func (o *Example) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(ExampleStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type ExampleStatus", value) + } + o.Status = cast + return nil + + case "custom": + cast, ok := value.(ExampleCustom) + if !ok { + return fmt.Errorf("cannot set custom type %#v, not of type ExampleCustom", value) + } + o.Custom = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Example) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Example) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Example) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Example) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Example) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Example) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Example) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Example) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Example) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Example) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Example) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Example) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Example) DeepCopy() *Example { + cpy := &Example{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Example) DeepCopyInto(dst *Example) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) + o.Custom.DeepCopyInto(&dst.Custom) +} + +// Interface compliance compile-time check +var _ resource.Object = &Example{} + +// +k8s:openapi-gen=true +type ExampleList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Example `json:"items" yaml:"items"` +} + +func (o *ExampleList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ExampleList) Copy() resource.ListObject { + cpy := &ExampleList{ + TypeMeta: o.TypeMeta, + Items: make([]Example, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Example); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *ExampleList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *ExampleList) SetItems(items []resource.Object) { + o.Items = make([]Example, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Example) + } +} + +func (o *ExampleList) DeepCopy() *ExampleList { + cpy := &ExampleList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *ExampleList) DeepCopyInto(dst *ExampleList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &ExampleList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *ExampleSpec) DeepCopy() *ExampleSpec { + cpy := &ExampleSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *ExampleSpec) DeepCopyInto(dst *ExampleSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of ExampleStatus +func (s *ExampleStatus) DeepCopy() *ExampleStatus { + cpy := &ExampleStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies ExampleStatus into another ExampleStatus object +func (s *ExampleStatus) DeepCopyInto(dst *ExampleStatus) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of ExampleCustom +func (s *ExampleCustom) DeepCopy() *ExampleCustom { + cpy := &ExampleCustom{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies ExampleCustom into another ExampleCustom object +func (s *ExampleCustom) DeepCopyInto(dst *ExampleCustom) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_schema_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_schema_gen.go new file mode 100644 index 00000000000..e1f7dca0b02 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaExample = resource.NewSimpleSchema("example.grafana.app", "v1alpha1", &Example{}, &ExampleList{}, resource.WithKind("Example"), + resource.WithPlural("examples"), resource.WithScope(resource.NamespacedScope)) + kindExample = resource.Kind{ + Schema: schemaExample, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &ExampleJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func ExampleKind() resource.Kind { + return kindExample +} + +// Schema returns a resource.SimpleSchema representation of Example +func ExampleSchema() *resource.SimpleSchema { + return schemaExample +} + +// Interface compliance checks +var _ resource.Schema = kindExample diff --git a/apps/example/pkg/apis/example/v1alpha1/example_spec_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_spec_gen.go new file mode 100644 index 00000000000..3f73653ac1c --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_spec_gen.go @@ -0,0 +1,34 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// #DefinedType is a re-usable definition for us to use in our schema. +// Fields leading with # are definitions in CUE and won't be included in the generated types. +// +k8s:openapi-gen=true +type ExampleDefinedType struct { + // Info is information about this entry. This comment, like all comments + // on fields or definitions, will be copied into the generated types as well. + Info string `json:"info"` + // Next is an optional next element in the DefinedType, allowing for a self-referential + // linked-list like structure. The ? in the field makes this optional. + Next *ExampleDefinedType `json:"next,omitempty"` +} + +// NewExampleDefinedType creates a new ExampleDefinedType object. +func NewExampleDefinedType() *ExampleDefinedType { + return &ExampleDefinedType{} +} + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +// +k8s:openapi-gen=true +type ExampleSpec struct { + // Example fields + FirstField string `json:"firstField"` + SecondField int64 `json:"secondField"` + List *ExampleDefinedType `json:"list,omitempty"` +} + +// NewExampleSpec creates a new ExampleSpec object. +func NewExampleSpec() *ExampleSpec { + return &ExampleSpec{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/example_status_gen.go b/apps/example/pkg/apis/example/v1alpha1/example_status_gen.go new file mode 100644 index 00000000000..ced0f82949e --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/example_status_gen.go @@ -0,0 +1,48 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type ExamplestatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State ExampleStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewExamplestatusOperatorState creates a new ExamplestatusOperatorState object. +func NewExamplestatusOperatorState() *ExamplestatusOperatorState { + return &ExamplestatusOperatorState{} +} + +// status is where state and status information which may be used or updated by the operator or back-end should be placed +// If you do not have any such information, you do not need to include this field, +// however, as mentioned above, certain fields will be added by the kind system regardless. +// +k8s:openapi-gen=true +type ExampleStatus struct { + LastObservedGeneration int64 `json:"lastObservedGeneration"` + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]ExamplestatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewExampleStatus creates a new ExampleStatus object. +func NewExampleStatus() *ExampleStatus { + return &ExampleStatus{} +} + +// +k8s:openapi-gen=true +type ExampleStatusOperatorStateState string + +const ( + ExampleStatusOperatorStateStateSuccess ExampleStatusOperatorStateState = "success" + ExampleStatusOperatorStateStateInProgress ExampleStatusOperatorStateState = "in_progress" + ExampleStatusOperatorStateStateFailed ExampleStatusOperatorStateState = "failed" +) diff --git a/apps/example/pkg/apis/example/v1alpha1/getother_request_params_object_gen.go b/apps/example/pkg/apis/example/v1alpha1/getother_request_params_object_gen.go new file mode 100644 index 00000000000..25fa13336e2 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getother_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetOtherRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetOtherRequestParams `json:",inline"` +} + +func NewGetOtherRequestParamsObject() *GetOtherRequestParamsObject { + return &GetOtherRequestParamsObject{} +} + +func (o *GetOtherRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetOtherRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetOtherRequestParamsObject) DeepCopyInto(dst *GetOtherRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetOtherRequestParams := GetOtherRequestParams{} + _ = resource.CopyObjectInto(&dstGetOtherRequestParams, &o.GetOtherRequestParams) +} + +var _ runtime.Object = NewGetOtherRequestParamsObject() diff --git a/apps/example/pkg/apis/example/v1alpha1/getother_request_params_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getother_request_params_types_gen.go new file mode 100644 index 00000000000..09b886b1df7 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getother_request_params_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +type GetOtherRequestParams struct { + Message *string `json:"message,omitempty"` +} + +// NewGetOtherRequestParams creates a new GetOtherRequestParams object. +func NewGetOtherRequestParams() *GetOtherRequestParams { + return &GetOtherRequestParams{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/getother_response_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getother_response_types_gen.go new file mode 100644 index 00000000000..e6c7bdf6ad8 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getother_response_types_gen.go @@ -0,0 +1,13 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type GetOther struct { + Message string `json:"message"` +} + +// NewGetOther creates a new GetOther object. +func NewGetOther() *GetOther { + return &GetOther{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_object_gen.go b/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_object_gen.go new file mode 100644 index 00000000000..83cdd19ad7f --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetSomethingRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetSomethingRequestParams `json:",inline"` +} + +func NewGetSomethingRequestParamsObject() *GetSomethingRequestParamsObject { + return &GetSomethingRequestParamsObject{} +} + +func (o *GetSomethingRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetSomethingRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetSomethingRequestParamsObject) DeepCopyInto(dst *GetSomethingRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetSomethingRequestParams := GetSomethingRequestParams{} + _ = resource.CopyObjectInto(&dstGetSomethingRequestParams, &o.GetSomethingRequestParams) +} + +var _ runtime.Object = NewGetSomethingRequestParamsObject() diff --git a/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_types_gen.go new file mode 100644 index 00000000000..30ff91085ce --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getsomething_request_params_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +type GetSomethingRequestParams struct { + Message *string `json:"message,omitempty"` +} + +// NewGetSomethingRequestParams creates a new GetSomethingRequestParams object. +func NewGetSomethingRequestParams() *GetSomethingRequestParams { + return &GetSomethingRequestParams{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/getsomething_response_body_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getsomething_response_body_types_gen.go new file mode 100644 index 00000000000..c785f9c4ecd --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getsomething_response_body_types_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type GetSomethingBody struct { + Namespace string `json:"namespace"` + Message string `json:"message"` +} + +// NewGetSomethingBody creates a new GetSomethingBody object. +func NewGetSomethingBody() *GetSomethingBody { + return &GetSomethingBody{} +} diff --git a/apps/example/pkg/apis/example/v1alpha1/getsomething_response_object_types_gen.go b/apps/example/pkg/apis/example/v1alpha1/getsomething_response_object_types_gen.go new file mode 100644 index 00000000000..2e6ce668030 --- /dev/null +++ b/apps/example/pkg/apis/example/v1alpha1/getsomething_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type GetSomething struct { + metav1.TypeMeta `json:",inline"` + GetSomethingBody `json:",inline"` +} + +func NewGetSomething() *GetSomething { + return &GetSomething{} +} + +func (t *GetSomethingBody) DeepCopyInto(dst *GetSomethingBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetSomething) DeepCopyObject() runtime.Object { + dst := NewGetSomething() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetSomething) DeepCopyInto(dst *GetSomething) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetSomethingBody.DeepCopyInto(&dst.GetSomethingBody) +} + +var _ runtime.Object = NewGetSomething() diff --git a/apps/example/pkg/apis/example_manifest.go b/apps/example/pkg/apis/example_manifest.go new file mode 100644 index 00000000000..eab0ee792ab --- /dev/null +++ b/apps/example/pkg/apis/example_manifest.go @@ -0,0 +1,374 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + v0alpha1 "github.com/grafana/grafana/apps/example/pkg/apis/example/v0alpha1" + v1alpha1 "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" +) + +var ( + rawSchemaExamplev0alpha1 = []byte(`{"Example":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"spec":{"additionalProperties":false,"description":"Spec is the schema of our resource. The spec should include all the user-editable information for the kind.","properties":{"firstField":{"type":"integer"}},"required":["firstField"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"lastObservedGeneration":{"type":"integer"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"required":["lastObservedGeneration"],"type":"object"}}`) + versionSchemaExamplev0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaExamplev0alpha1, &versionSchemaExamplev0alpha1) + rawSchemaExamplev1alpha1 = []byte(`{"DefinedType":{"additionalProperties":false,"description":"#DefinedType is a re-usable definition for us to use in our schema.\nFields leading with # are definitions in CUE and won't be included in the generated types.","properties":{"info":{"description":"Info is information about this entry. This comment, like all comments\non fields or definitions, will be copied into the generated types as well.","type":"string"},"next":{"$ref":"#/components/schemas/DefinedType","description":"Next is an optional next element in the DefinedType, allowing for a self-referential\nlinked-list like structure. The ? in the field makes this optional."}},"required":["info"],"type":"object"},"Example":{"properties":{"custom":{"$ref":"#/components/schemas/custom"},"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"custom":{"additionalProperties":false,"description":"Custom is a subresource that will be stored the same way status is stored,\nand requires using the /custom route to update.\nIts content is returned as part of a GET to the resource itself, just like with status.\nTo route a subresource to an arbitrary handler, use the 'routes' field instead (see below).\nmetadata if where kind- and schema-specific metadata goes. This is converted into typed annotations\nwith getters and setters by the code generation.\nmetadata: {\n\tkindSpecificField: string\n}","properties":{"myField":{"type":"string"},"otherField":{"type":"string"}},"required":["myField","otherField"],"type":"object"},"spec":{"additionalProperties":false,"description":"Spec is the schema of our resource. The spec should include all the user-editable information for the kind.","properties":{"firstField":{"description":"Example fields","type":"string"},"list":{"$ref":"#/components/schemas/DefinedType"},"secondField":{"type":"integer"}},"required":["firstField","secondField"],"type":"object"},"status":{"additionalProperties":false,"description":"status is where state and status information which may be used or updated by the operator or back-end should be placed\nIf you do not have any such information, you do not need to include this field,\nhowever, as mentioned above, certain fields will be added by the kind system regardless.","properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"lastObservedGeneration":{"type":"integer"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"required":["lastObservedGeneration"],"type":"object"}}`) + versionSchemaExamplev1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaExamplev1alpha1, &versionSchemaExamplev1alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "example", + Group: "example.grafana.app", + PreferredVersion: "v1alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v0alpha1", + Served: false, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Example", + Plural: "Examples", + Scope: "Namespaced", + Conversion: true, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + Mutation: &app.MutationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaExamplev0alpha1, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + }, + }, + + { + Name: "v1alpha1", + Served: false, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Example", + Plural: "Examples", + Scope: "Namespaced", + Conversion: true, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + Mutation: &app.MutationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaExamplev1alpha1, + Routes: map[string]spec3.PathProps{ + "foo": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getFoo", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "message", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Description: "The response type for the GET /foo method. This will generate a go type, and will also be used for the OpenAPI definition for the route.", + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "message", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{ + "/something": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getSomething", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "message", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "namespace", + "message", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + Cluster: map[string]spec3.PathProps{ + "/other": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getOther", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "message", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "message", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("example") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "Example/v0alpha1": v0alpha1.ExampleKind(), + "Example/v1alpha1": v1alpha1.ExampleKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{ + + "v1alpha1|Example|foo|GET": v1alpha1.GetFoo{}, + + "v1alpha1||/something|GET": v1alpha1.GetSomething{}, + "v1alpha1||other|GET": v1alpha1.GetOther{}, +} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{ + "v1alpha1|Example|foo|GET": &v1alpha1.GetFooRequestParamsObject{}, +} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/example/pkg/app/app.go b/apps/example/pkg/app/app.go new file mode 100644 index 00000000000..3d189ca6155 --- /dev/null +++ b/apps/example/pkg/app/app.go @@ -0,0 +1,143 @@ +package app + +import ( + "fmt" + "log/slog" + "os" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana-app-sdk/simple" + "k8s.io/apimachinery/pkg/runtime/schema" + + examplev0alpha1 "github.com/grafana/grafana/apps/example/pkg/apis/example/v0alpha1" + examplev1alpha1 "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" +) + +// New creates a new instance of the Example App. It gets called after the app's APIs have been registered, +// and is used for routing non-storage API requests, admission control, conversion, and can run +// reconcilers on kinds. +func New(cfg app.Config) (app.App, error) { + // APIPath needs to be set to `/apis`, as it defaults to empty + cfg.KubeConfig.APIPath = "/apis" + // We create a client to work with our Example kind in our reconciler + client, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()).ClientFor(examplev1alpha1.ExampleKind()) + if err != nil { + return nil, fmt.Errorf("unable to create example client: %w", err) + } + var reconciler operator.Reconciler + exampleConfig, ok := cfg.SpecificConfig.(*ExampleConfig) + if ok && exampleConfig.EnableReconciler { + reconciler = NewExampleReconciler(client) + // Set the default logger if the reconciler is enabled--this should be done in grafana's API server handling instead, + // and will be corrected in a future PR + logging.DefaultLogger = logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, // Temporarily hardcoded to debug for the example + })) + } + + // This is the configuration for our App. + simpleConfig := simple.AppConfig{ + Name: "example", + KubeConfig: cfg.KubeConfig, + // ManagedKinds is the list of all kinds our app manages (the kinds owned by our app). + // Here, a Kind is defined as a distinct Group, Version, and Kind combination, + // so for each version of our Example kind, we need to add it to this list. + // Each kind can also have admission control attached to it--different versions can have different admission control attached. + // Handlers for custom routes defined in the manifest for the kind go here--this is where they actuall get routed, + // they are only defined in the manifest. + // Reconcilers and/or Watchers are also attached here, though they should only be attached to a single version per kind. + ManagedKinds: []simple.AppManagedKind{ + { + Kind: examplev0alpha1.ExampleKind(), + // Validator is run on ingress and is it returns an error the request is rejected + Validator: NewValidator(), + // Mutator is run on ingress and makes changes to the input object + Mutator: NewMutator(), + }, + { + Kind: examplev1alpha1.ExampleKind(), + // We only want the reconciler on one version of our kind, and it's usually best to use the latest + // We'll receive events for every example object, regardless of version used in the API, + // it will convert them to the version used for the reconciler. + Reconciler: reconciler, + // By default, reconcilers for ManagedKinds are wrapped in + ReconcileOptions: simple.BasicReconcileOptions{ + // Namespace is the namespace your reconciler will watch. + // It defaults to all, so this isn't necessary to specify the way we do here. + Namespace: resource.NamespaceAll, + // We can optionally filter our reconciler to only get events for Example resources which + // satisfy the following label filters + // LabelFilters: []string{"foo=bar"}, + // By default, reconcilers for ManagedKinds are wrapped in the app-sdk's OpinionatedReconciler. + // To turn this functionality off, you can set UsePlain to false + // UsePlain: true, + }, + // Validator is run on ingress and is it returns an error the request is rejected + Validator: NewValidator(), + // Mutator is run on ingress and makes changes to the input object + Mutator: NewMutator(), + // We defined this route in our CUE, but we need to actually define the HTTP handler for it. + CustomRoutes: simple.AppCustomRouteHandlers{ + { + Path: "foo", + Method: "GET", + }: ExampleGetFooHandler, + }, + }, + }, + // Conversion for kinds is defined for all versions of a kind at once. + // This interface may change in the future, see https://github.com/grafana/grafana-app-sdk/issues/617 + Converters: map[schema.GroupKind]simple.Converter{ + { + Group: cfg.ManifestData.Group, + Kind: examplev0alpha1.ExampleKind().Kind(), + }: NewExampleConverter(), + }, + // VersionedCustomRoutes are the custom route handlers for routes defined at the version level of the manifest + // instead of for a specific kind. This are sometimes referred to as "resource routes" + // (as opposed to "subresource routes" which are attached to kinds). + VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{ + "v1alpha1": { + { + Namespaced: true, + Path: "something", + Method: "GET", + }: GetSomethingHandler, + { + Namespaced: false, + Path: "other", + Method: "GET", + }: GetOtherHandler, + }, + }, + } + + a, err := simple.NewApp(simpleConfig) + if err != nil { + return nil, err + } + + // This makes it easier to catch problems at startup, rather than when something doesn't behave as expected. + // ValidateManifest will ensure that the capabilities you define in your simple.AppConfig + // match the capabilities described in the AppManifest. + err = a.ValidateManifest(cfg.ManifestData) + if err != nil { + return nil, err + } + + return a, nil +} + +func GetKinds() map[schema.GroupVersion][]resource.Kind { + gv := schema.GroupVersion{ + Group: examplev1alpha1.ExampleKind().Group(), + Version: examplev1alpha1.ExampleKind().Version(), + } + return map[schema.GroupVersion][]resource.Kind{ + gv: {examplev1alpha1.ExampleKind()}, + } +} diff --git a/apps/example/pkg/app/authorizer.go b/apps/example/pkg/app/authorizer.go new file mode 100644 index 00000000000..1848d47898b --- /dev/null +++ b/apps/example/pkg/app/authorizer.go @@ -0,0 +1,55 @@ +package app + +import ( + "context" + "fmt" + "regexp" + + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +var namespacedSomethingRouteMatcher = regexp.MustCompile(fmt.Sprintf(`^/apis/%s/%s/namespaces/([^\/]+)/something$`, v1alpha1.APIGroup, v1alpha1.APIVersion)) + +// GetAuthorizer returns an authorizer for all kinds managed by the example app. +// It must be added to the installer in pkg/registry/apps/example/register.go to be used +func GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc( + func(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + // require a user + u, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + // check if is admin + if u.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + + // Only allow admins to call the custom subresource + if attr.GetSubresource() == "custom" { + return authorizer.DecisionDeny, "forbidden", nil + } + + // Only allow admins to call the namespaced and cluster routes + // There's no easy way to check that from attrs like with GetSubresource(), + // so we look at the full path and check + if namespacedSomethingRouteMatcher.MatchString(attr.GetPath()) { + return authorizer.DecisionDeny, "forbidden", nil + } + if attr.GetPath() == fmt.Sprintf("/apis/%s/%s/other", v1alpha1.APIGroup, v1alpha1.APIVersion) { + return authorizer.DecisionDeny, "forbidden", nil + } + + // Otherwise, allow + return authorizer.DecisionAllow, "", nil + }, + ) +} diff --git a/apps/example/pkg/app/config.go b/apps/example/pkg/app/config.go new file mode 100644 index 00000000000..bbbedf54202 --- /dev/null +++ b/apps/example/pkg/app/config.go @@ -0,0 +1,7 @@ +package app + +// ExampleConfig is an example app-specific config type +type ExampleConfig struct { + EnableReconciler bool + EnableSomeFeature bool +} diff --git a/apps/example/pkg/app/conversion.go b/apps/example/pkg/app/conversion.go new file mode 100644 index 00000000000..1bd7f6addd9 --- /dev/null +++ b/apps/example/pkg/app/conversion.go @@ -0,0 +1,126 @@ +package app + +import ( + "bytes" + "errors" + "fmt" + "strconv" + + "github.com/grafana/grafana-app-sdk/k8s" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana-app-sdk/simple" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v0alpha1" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var _ simple.Converter = NewExampleConverter() + +type ExampleConverter struct{} + +func NewExampleConverter() *ExampleConverter { + return &ExampleConverter{} +} + +// Convert converts an object from an arbitrary input version slice of bytes +// to a target version, and returns the JSON bytes of that version. +func (e *ExampleConverter) Convert(obj k8s.RawKind, targetAPIVersion string) ([]byte, error) { + srcGVK := schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) + dstGVK := schema.FromAPIVersionAndKind(targetAPIVersion, v1alpha1.ExampleKind().Kind()) + if srcGVK.Group != v1alpha1.APIGroup { + // This should never happen, but check just in case + return nil, fmt.Errorf("wrong group to convert example.grafana.app, got %s", srcGVK.Group) + } + if srcGVK.Kind != v1alpha1.ExampleKind().Kind() { + // This should also never happen, but check just in case + return nil, fmt.Errorf("wrong kind to convert Example, got %s", srcGVK.Kind) + } + if srcGVK == dstGVK { + // This should never happen, but if it does no conversion is necessary, we can return the input + return obj.Raw, nil + } + + // Check source version + switch srcGVK.Version { + case v0alpha1.APIVersion: + srcKind := v0alpha1.ExampleKind() + uncastSrcObj, err := srcKind.Read(bytes.NewReader(obj.Raw), resource.KindEncodingJSON) + if err != nil { + return nil, fmt.Errorf("unable to parse JSON bytes into %s: %w", srcGVK.String(), err) + } + srcObj, ok := uncastSrcObj.(*v0alpha1.Example) + if !ok { + return nil, errors.New("read object was not of type *v0alpha1.Example") + } + switch dstGVK.Version { + case v1alpha1.APIVersion: + dstObj := &v1alpha1.Example{} + // Set Type metadata + dstObj.SetGroupVersionKind(dstGVK) + // Copy Object metadata + srcObj.ObjectMeta.DeepCopyInto(&dstObj.ObjectMeta) + // Copy spec and status + dstObj.Spec.FirstField = strconv.Itoa(int(srcObj.Spec.FirstField)) + dstObj.Status.LastObservedGeneration = srcObj.Status.LastObservedGeneration + dstObj.Status.AdditionalFields = srcObj.Status.AdditionalFields + if srcObj.Status.OperatorStates != nil { + dstObj.Status.OperatorStates = make(map[string]v1alpha1.ExamplestatusOperatorState) + for k, v := range srcObj.Status.OperatorStates { + dstObj.Status.OperatorStates[k] = v1alpha1.ExamplestatusOperatorState{ + LastEvaluation: v.LastEvaluation, + State: v1alpha1.ExampleStatusOperatorStateState(v.State), + DescriptiveState: v.DescriptiveState, + Details: v.Details, + } + } + } + dstKind := v1alpha1.ExampleKind() + buf := &bytes.Buffer{} + err := dstKind.Write(dstObj, buf, resource.KindEncodingJSON) + return buf.Bytes(), err + default: + return nil, fmt.Errorf("unknown target version %s", dstGVK.Version) + } + case v1alpha1.APIVersion: + srcKind := v1alpha1.ExampleKind() + uncastSrcObj, err := srcKind.Read(bytes.NewReader(obj.Raw), resource.KindEncodingJSON) + if err != nil { + return nil, fmt.Errorf("unable to parse JSON bytes into %s: %w", srcGVK.String(), err) + } + srcObj, ok := uncastSrcObj.(*v1alpha1.Example) + if !ok { + return nil, errors.New("read object was not of type *v1alpha1.Example") + } + switch dstGVK.Version { + case v0alpha1.APIVersion: + dstObj := &v0alpha1.Example{} + // Set Type metadata + dstObj.SetGroupVersionKind(dstGVK) + // Copy Object metadata + srcObj.ObjectMeta.DeepCopyInto(&dstObj.ObjectMeta) + // Copy spec and status + castInt, _ := strconv.Atoi(srcObj.Spec.FirstField) // Lossy backwards conversion + dstObj.Spec.FirstField = int64(castInt) + dstObj.Status.LastObservedGeneration = srcObj.Status.LastObservedGeneration + dstObj.Status.AdditionalFields = srcObj.Status.AdditionalFields + if srcObj.Status.OperatorStates != nil { + dstObj.Status.OperatorStates = make(map[string]v0alpha1.ExamplestatusOperatorState) + for k, v := range srcObj.Status.OperatorStates { + dstObj.Status.OperatorStates[k] = v0alpha1.ExamplestatusOperatorState{ + LastEvaluation: v.LastEvaluation, + State: v0alpha1.ExampleStatusOperatorStateState(v.State), + DescriptiveState: v.DescriptiveState, + Details: v.Details, + } + } + } + dstKind := v0alpha1.ExampleKind() + buf := &bytes.Buffer{} + err := dstKind.Write(dstObj, buf, resource.KindEncodingJSON) + return buf.Bytes(), err + default: + return nil, fmt.Errorf("unknown target version %s", dstGVK.Version) + } + } + return nil, fmt.Errorf("unknown source version %s", srcGVK.Version) +} diff --git a/apps/example/pkg/app/mutation.go b/apps/example/pkg/app/mutation.go new file mode 100644 index 00000000000..a04c2b9f312 --- /dev/null +++ b/apps/example/pkg/app/mutation.go @@ -0,0 +1,31 @@ +package app + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/simple" +) + +var _ simple.KindMutator = NewMutator() + +type Mutator struct{} + +func NewMutator() *Mutator { + return &Mutator{} +} + +// Mutate makes modifications to an input object from the API, and returns the changed object. +// This mutation will be done on every request, so it can be used to add or update things like labels +// or annotations. Here, we add an annotation noting the last resourceVersion this was called for. +func (m *Mutator) Mutate(ctx context.Context, req *app.AdmissionRequest) (*app.MutatingResponse, error) { + annotations := req.Object.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations["example.grafana.app/mutated"] = req.Object.GetResourceVersion() + req.Object.SetAnnotations(annotations) + return &app.MutatingResponse{ + UpdatedObject: req.Object, + }, nil +} diff --git a/apps/example/pkg/app/reconciler.go b/apps/example/pkg/app/reconciler.go new file mode 100644 index 00000000000..66399c62bf8 --- /dev/null +++ b/apps/example/pkg/app/reconciler.go @@ -0,0 +1,58 @@ +package app + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" +) + +// ExampleReconciler wraps TypedReconciler to simplify some of our reconciliation logic, +// as TypedReconciler will handle type checking of the input object for us. +type ExampleReconciler struct { + operator.TypedReconciler[*v1alpha1.Example] + client resource.Client +} + +func NewExampleReconciler(client resource.Client) *ExampleReconciler { + reconciler := ExampleReconciler{ + TypedReconciler: operator.TypedReconciler[*v1alpha1.Example]{}, + client: client, + } + reconciler.ReconcileFunc = reconciler.doReconcile + return &reconciler +} + +// doReconcile is the main reconciliation loop for our app's Example reconciler. +// All it does is print a log message and then update the last observed generation in the status +// (if the request is a DELETE, it doesn't try to update the status, as the update would fail). +func (e *ExampleReconciler) doReconcile(ctx context.Context, req operator.TypedReconcileRequest[*v1alpha1.Example]) (operator.ReconcileResult, error) { + if req.Object.GetGeneration() == req.Object.Status.LastObservedGeneration { + // Skip if we've already processed this spec + return operator.ReconcileResult{}, nil + } + + logging.FromContext(ctx).Info("reconciling example", "name", req.Object.GetName(), "namespace", req.Object.GetNamespace(), "action", operator.ResourceActionFromReconcileAction(req.Action)) + + // If this is a delete, we don't need to do anything + if req.Action == operator.ReconcileActionDeleted { + return operator.ReconcileResult{}, nil + } + + // Update the status. + // We use resource.UpdateObject here to handle conflicts when doing the update, + // as it gets the current state, performs our update function, then pushes to the remote + _, err := resource.UpdateObject(ctx, e.client, req.Object.GetStaticMetadata().Identifier(), func(obj *v1alpha1.Example, _ bool) (*v1alpha1.Example, error) { + obj.Status.LastObservedGeneration = req.Object.GetGeneration() + return obj, nil + }, resource.UpdateOptions{ + Subresource: "status", + }) + if err != nil { + return operator.ReconcileResult{}, err + } + + return operator.ReconcileResult{}, nil +} diff --git a/apps/example/pkg/app/routes.go b/apps/example/pkg/app/routes.go new file mode 100644 index 00000000000..eb9206b0ce3 --- /dev/null +++ b/apps/example/pkg/app/routes.go @@ -0,0 +1,50 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" +) + +// ExampleGetFooHandler handles requests for the GET /foo subresource route +func ExampleGetFooHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + message := "Hello, world!" + return json.NewEncoder(writer).Encode(v1alpha1.GetFoo{ + GetFooBody: v1alpha1.GetFooBody{ + Message: message, + }, + }) +} + +// GetSomethingHandler handles requests for the GET /something resource route +func GetSomethingHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + message := "This is a namespaced route" + if request.URL.Query().Has("message") { + message = request.URL.Query().Get("message") + } + return json.NewEncoder(writer).Encode(v1alpha1.GetSomething{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.APIGroup, v1alpha1.APIVersion), + }, + GetSomethingBody: v1alpha1.GetSomethingBody{ + Namespace: request.ResourceIdentifier.Namespace, + Message: message, + }, + }) +} + +// GetOtherHandler handles requests for the GET /other cluster-scoped resource route +func GetOtherHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + message := "This is a cluster route" + if request.URL.Query().Has("message") { + message = request.URL.Query().Get("message") + } + return json.NewEncoder(writer).Encode(v1alpha1.GetOther{ + Message: message, + }) +} diff --git a/apps/example/pkg/app/validation.go b/apps/example/pkg/app/validation.go new file mode 100644 index 00000000000..74dad42f9ca --- /dev/null +++ b/apps/example/pkg/app/validation.go @@ -0,0 +1,28 @@ +package app + +import ( + "context" + "errors" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/simple" +) + +var _ simple.KindValidator = NewValidator() + +// Validator implements simple.KindValidator +type Validator struct{} + +func NewValidator() *Validator { + return &Validator{} +} + +// Validate runs any kind of validation on incoming objects, +// and returns an error to reject the request. +// Here, we just reject any Example resource which is named "invalid" +func (v *Validator) Validate(ctx context.Context, req *app.AdmissionRequest) error { + if req.Object.GetName() == "invalid" { + return errors.New("example cannot be named 'invalid'") + } + return nil +} diff --git a/apps/example/plugin/src/generated/example/v0alpha1/example_object_gen.ts b/apps/example/plugin/src/generated/example/v0alpha1/example_object_gen.ts new file mode 100644 index 00000000000..dd344a9b8b2 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v0alpha1/example_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Example { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/example/plugin/src/generated/example/v0alpha1/types.metadata.gen.ts b/apps/example/plugin/src/generated/example/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v0alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/example/plugin/src/generated/example/v0alpha1/types.spec.gen.ts b/apps/example/plugin/src/generated/example/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..aee6e7ddc96 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v0alpha1/types.spec.gen.ts @@ -0,0 +1,11 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +export interface Spec { + firstField: number; +} + +export const defaultSpec = (): Spec => ({ + firstField: 0, +}); + diff --git a/apps/example/plugin/src/generated/example/v0alpha1/types.status.gen.ts b/apps/example/plugin/src/generated/example/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..2b1c7ecb645 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v0alpha1/types.status.gen.ts @@ -0,0 +1,32 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + lastObservedGeneration: number; + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ + lastObservedGeneration: 0, +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/example_object_gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/example_object_gen.ts new file mode 100644 index 00000000000..1d45847ce0f --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/example_object_gen.ts @@ -0,0 +1,51 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; +import { Custom } from './types.custom.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Example { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; + custom: Custom; +} diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.custom.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.custom.gen.ts new file mode 100644 index 00000000000..5de21c36556 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.custom.gen.ts @@ -0,0 +1,21 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// Custom is a subresource that will be stored the same way status is stored, +// and requires using the /custom route to update. +// Its content is returned as part of a GET to the resource itself, just like with status. +// To route a subresource to an arbitrary handler, use the 'routes' field instead (see below). +// metadata if where kind- and schema-specific metadata goes. This is converted into typed annotations +// with getters and setters by the code generation. +// metadata: { +// kindSpecificField: string +// } +export interface Custom { + myField: string; + otherField: string; +} + +export const defaultCustom = (): Custom => ({ + myField: "", + otherField: "", +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.metadata.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.routes.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.routes.gen.ts new file mode 100644 index 00000000000..145a7070c14 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.routes.gen.ts @@ -0,0 +1,35 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// routes contains subresource routes for the kind, which are exposed as HTTP handlers on `examples//`. +// This allows you to add additional non-storage-based handlers to your kind. +// These should only be used if the behavior cannot be accomplished by reconciliation on storage events. +export interface Routes { + // This will add a handler for /foo on the resource + foo: { + // GET request handler. A subresource route can have multiple methods attached to it. + // Allowed values are GET, POST, PUT, DELETE, PATCH, HEAD, and OPTIONS + GET: { + // The response type for the GET /foo method. + // This will generate a go type, and will also be used for the OpenAPI definition for the route. + response: { + message: string; + }; + request: { + message?: string; + }; + }; + }; +} + +export const defaultRoutes = (): Routes => ({ + foo: { + GET: { + response: { + message: "", +}, + request: { +}, +}, +}, +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.spec.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..1a490c64cc7 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.spec.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// #DefinedType is a re-usable definition for us to use in our schema. +// Fields leading with # are definitions in CUE and won't be included in the generated types. +export interface DefinedType { + // Info is information about this entry. This comment, like all comments + // on fields or definitions, will be copied into the generated types as well. + info: string; + // Next is an optional next element in the DefinedType, allowing for a self-referential + // linked-list like structure. The ? in the field makes this optional. + next?: DefinedType; +} + +export const defaultDefinedType = (): DefinedType => ({ + info: "", +}); + +// Spec is the schema of our resource. The spec should include all the user-editable information for the kind. +export interface Spec { + // Example fields + firstField: string; + secondField: number; + list?: DefinedType; +} + +export const defaultSpec = (): Spec => ({ + firstField: "", + secondField: 0, +}); + diff --git a/apps/example/plugin/src/generated/example/v1alpha1/types.status.gen.ts b/apps/example/plugin/src/generated/example/v1alpha1/types.status.gen.ts new file mode 100644 index 00000000000..712de3d9076 --- /dev/null +++ b/apps/example/plugin/src/generated/example/v1alpha1/types.status.gen.ts @@ -0,0 +1,35 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +// status is where state and status information which may be used or updated by the operator or back-end should be placed +// If you do not have any such information, you do not need to include this field, +// however, as mentioned above, certain fields will be added by the kind system regardless. +export interface Status { + lastObservedGeneration: number; + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ + lastObservedGeneration: 0, +}); + diff --git a/apps/example/plugin/src/generated/examplekind/v1alpha1/examplekind_object_gen.ts b/apps/example/plugin/src/generated/examplekind/v1alpha1/examplekind_object_gen.ts new file mode 100644 index 00000000000..c77f8842a68 --- /dev/null +++ b/apps/example/plugin/src/generated/examplekind/v1alpha1/examplekind_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface ExampleKind { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/example/plugin/src/generated/examplekind/v1alpha1/types.metadata.gen.ts b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/example/plugin/src/generated/examplekind/v1alpha1/types.spec.gen.ts b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..adc7830a1ac --- /dev/null +++ b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.spec.gen.ts @@ -0,0 +1,25 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// spec is the schema of our resource. The spec should include all the user-editable information for the kind. +// status is where state and status information which may be used or updated by the operator or back-end should be placed +// If you do not have any such information, you do not need to include this field, +// however, as mentioned above, certain fields will be added by the kind system regardless. +// status: { +// currentState: string +// } +// metadata if where kind- and schema-specific metadata goes. This is converted into typed annotations +// with getters and setters by the code generation. +// metadata: { +// kindSpecificField: string +// } +export interface Spec { + // Example fields + firstField: string; + secondField: number; +} + +export const defaultSpec = (): Spec => ({ + firstField: "", + secondField: 0, +}); + diff --git a/apps/example/plugin/src/generated/examplekind/v1alpha1/types.status.gen.ts b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/example/plugin/src/generated/examplekind/v1alpha1/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 89d24838284..f87baeab415 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/folder go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 + github.com/grafana/grafana-app-sdk v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -24,7 +24,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.46.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -50,12 +50,12 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 3cb21ccfd3e..9b1b0735938 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -126,8 +126,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -138,14 +138,14 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index d0934bdf950..bc185ae88f3 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -46,8 +46,8 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.47.0 - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/prometheus/client_golang v1.23.2 @@ -59,8 +59,8 @@ require ( require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go v0.121.1 // indirect - cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go v0.121.4 // indirect + cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect @@ -79,8 +79,8 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect @@ -100,24 +100,24 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/at-wat/mqtt-go v0.19.4 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect - github.com/aws/aws-sdk-go-v2 v1.38.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect - github.com/aws/aws-sdk-go-v2/config v1.31.2 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.18.6 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect github.com/aws/smithy-go v1.23.1 // indirect github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -151,7 +151,6 @@ require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dgraph-io/badger/v4 v4.7.0 // indirect @@ -165,7 +164,7 @@ require ( github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad // indirect github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e // indirect github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect - github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 // indirect + github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect @@ -182,7 +181,7 @@ require ( github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.24.0 // indirect @@ -216,7 +215,6 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-migrate/migrate/v4 v4.7.0 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect @@ -229,14 +227,14 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/google/wire v0.7.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-aws-sdk v1.2.0 // indirect + github.com/grafana/grafana-aws-sdk v1.3.0 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect github.com/grafana/grafana-plugin-sdk-go v0.281.0 // indirect github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect @@ -284,7 +282,7 @@ require ( github.com/jhump/protoreflect v1.17.0 // indirect github.com/jmespath-community/go-jmespath v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -296,7 +294,6 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lestrrat-go/strftime v1.0.4 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect @@ -362,7 +359,6 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect @@ -383,13 +379,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tetratelabs/wazero v1.8.2 // indirect github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect - github.com/tjhop/slog-gokit v0.1.3 // indirect + github.com/tjhop/slog-gokit v0.1.5 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect - github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect - github.com/unknwon/com v1.0.1 // indirect - github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect - github.com/urfave/cli v1.22.17 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -399,10 +391,9 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect @@ -419,7 +410,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.59.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect go.opentelemetry.io/otel/log v0.12.2 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect @@ -433,31 +424,30 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - gocloud.dev v0.42.0 // indirect - golang.org/x/crypto v0.42.0 // indirect + gocloud.dev v0.43.0 // indirect + golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/api v0.235.0 // indirect - google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/api v0.242.0 // indirect + google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/mail.v2 v2.3.1 // indirect @@ -483,5 +473,5 @@ require ( sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect - xorm.io/builder v0.3.6 // indirect + xorm.io/builder v0.3.13 // indirect ) diff --git a/apps/iam/go.sum b/apps/iam/go.sum index c8e63a53524..31148a98766 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -36,10 +36,10 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= -cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= -cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= -cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs= +cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s= +cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= +cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -95,6 +95,8 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 h1:qFYgLH2zZe3WHpQgUrzeazC+ebDebwAQqS9yE1cP5Bs= github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2/go.mod h1:09/ALd1AXCTCOfcJYD8+jIYKmFmi6PVCkTsipC18F7E= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= @@ -120,6 +122,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+ github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk= github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= @@ -151,12 +155,12 @@ github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -233,54 +237,54 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= -github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= +github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= -github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0= -github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= -github.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU= -github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 h1:6VFPH/Zi9xYFMJKPQOX5URYkQoXRWeJ7V/7Y6ZDYoms= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= +github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4= +github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8= +github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 h1:Nn3qce+OHZuMj/edx4its32uxedAmquCDxtZkrdeiD4= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3/go.mod h1:aqsLGsPs+rJfwDBwWHLcIV8F7AFcikFTPLwUD4RwORQ= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 h1:e5cbPZYTIY2nUEFieZUfVdINOiCTvChOMPfdLnmiLzs= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0/go.mod h1:UseIHRfrm7PqeZo6fcTb6FUCXzCnh1KJbQbmOfxArGM= github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 h1:IfMb3Ar8xEaWjgH/zeVHYD8izwJdQgRP5mKCTDt4GNk= github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= -github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc= -github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.2/go.mod h1:Pqd9k4TuespkireN206cK2QBsaBTL6X+VPAez5Qcijk= github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 h1:teOWtElLARLOhpYWwupjLbY9j5I/yZ/H1I8jg41An78= github.com/aws/aws-sdk-go-v2/service/oam v1.18.3/go.mod h1:wGhpdyftHX6/1U4egowHkYdypwBMjpb+KjAAprv6z20= github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:PwbxovpcJvb25k019bkibvJfCpCmIANOFrXZIFPmRzk= github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4= -github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s= @@ -425,6 +429,10 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -434,8 +442,10 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -497,14 +507,16 @@ github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTE github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= -github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 h1:LGTt2LtYX8vaai32d+c9L0sMcP+Dg9w1kO6+lbsxxYg= -github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33 h1:ScHTwNbcVC6JH1OSyXzj8S4w67BIpRXwTSjrac3/PSw= +github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33/go.mod h1:8pvvk5OLaLN9LLxghyczUapn/97l+mBgIb10qC1LG84= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= @@ -583,8 +595,8 @@ github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXg github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= +github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -593,6 +605,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA= github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw= github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs= @@ -644,7 +658,7 @@ github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -656,8 +670,6 @@ github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= -github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -695,8 +707,6 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -813,12 +823,9 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= -github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -844,12 +851,12 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxvTovycpQvDTfsNZ2rHh4XIIaccMnrIR4= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana-aws-sdk v1.2.0 h1:LLR4/g91WBuCRwm2cbWfCREq565+GxIFe08nqqIcIuw= -github.com/grafana/grafana-aws-sdk v1.2.0/go.mod h1:bBo7qOmM3f61vO+2JxTolNUph1l2TmtzmWcU9/Im+8A= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= +github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc= github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 h1:JOzchPgptwJdruYoed7x28lFDwhzs7kssResYsnC0iI= @@ -860,6 +867,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 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.281.0 h1:V8dGyatzcOLQeivFhBV2JWMwTSZH/clDnpfKG9p3dTA= github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4= +github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= +github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= @@ -870,8 +879,8 @@ github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLm github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000/go.mod h1:/ZklAgE1i4f3Z8uriXwESmCr1VLF8lBGaJspuaGuf78= github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= -github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 h1:cS0SlJGIlZbmDLctNj5vIYGemrJDLy25wwoiIyZWVN8= -github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b h1:rFjoqJFb2KxJ29K9ltuWRSsdA46SbN0GCxoQc36h5kg= +github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= 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.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= @@ -958,8 +967,9 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -971,8 +981,8 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= -github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= -github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/hashicorp/vault/api v1.20.0 h1:KQMHElgudOsr+IbJgmbjHnCTxEpKs9LnozA1D3nozU4= +github.com/hashicorp/vault/api v1.20.0/go.mod h1:GZ4pcjfzoOWpkJ3ijHNpEoAxKEsBJnVljyTe3jM2Sms= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -1033,8 +1043,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -1054,9 +1064,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ= github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o= -github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= @@ -1105,14 +1112,16 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/m3db/prometheus_remote_client_golang v0.4.4 h1:DsAIjVKoCp7Ym35tAOFL1OuMLIdIikAEHeNPHY+yyM8= github.com/m3db/prometheus_remote_client_golang v0.4.4/go.mod h1:wHfVbA3eAK6dQvKjCkHhusWYegCk3bDGkA15zymSHdc= github.com/madflojo/testcerts v1.4.0 h1:I09gN0C1ly9IgeVNcAqKk8RAKIJTe3QnFrrPBDyvzN4= github.com/madflojo/testcerts v1.4.0/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLDUsQhpmkuo0= -github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= -github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 h1:hQWBtNqRYrI7CWIaUSXXtNKR90KzcUA5uiuxFVWw7sU= @@ -1144,6 +1153,7 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -1195,8 +1205,20 @@ github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QR github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/mocktools/go-smtp-mock/v2 v2.5.1 h1:QcMJMChSgG1olVj4o6xxQFdrWzRjYNrcq660HAjd0wA= github.com/mocktools/go-smtp-mock/v2 v2.5.1/go.mod h1:Rr8M2njlxx//l5INl2+uESnsL2lDsL24teEykCrGfmE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1210,6 +1232,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWu github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -1317,6 +1341,8 @@ 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng= github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1392,8 +1418,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -1417,12 +1443,13 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3/go.mod h1:aJIMhRsunltJR926EB2MUg8qHemFQDreSB33pyto2Ps= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 h1:OfRzdxCzDhp+rsKWXuOO2I/quKMJ/+TQwVbIP/gltZg= github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92/go.mod h1:7/OT02F6S6I7v6WXb+IjhMuZEYfH/RJ5RwEWnEo5BMg= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -1431,11 +1458,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= @@ -1490,12 +1512,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00= +github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0= github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= @@ -1503,8 +1526,12 @@ github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574 github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE9IrvgBA2C1mRN1lc= github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o= -github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= +github.com/tjhop/slog-gokit v0.1.5 h1:ayloIUi5EK2QYB8eY4DOPO95/mRtMW42lUkp3quJohc= +github.com/tjhop/slog-gokit v0.1.5/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -1517,16 +1544,7 @@ github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= -github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4= -github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= -github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= -github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= -github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a h1:vcrhXnj9g9PIE+cmZgaPSwOyJ8MAQTRmsgGrB0x5rF4= -github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= -github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -1560,6 +1578,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -1598,20 +1618,18 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/collector/featuregate v1.43.0 h1:Aq8UR5qv1zNlbbkTyqv8kLJtnoQMq/sG1/jS9o1cCJI= -go.opentelemetry.io/collector/featuregate v1.43.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= -go.opentelemetry.io/collector/pdata v1.43.0 h1:zVkj2hcjiMLwX+QDDNwb7iTh3LBjNXKv2qPSgj1Rzb4= -go.opentelemetry.io/collector/pdata v1.43.0/go.mod h1:KsJzdDG9e5BaHlmYr0sqdSEKeEiSfKzoF+rdWU7J//w= +go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= +go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/pdata v1.44.0 h1:q/EfWDDKrSaf4hjTIzyPeg1ZcCRg1Uj7VTFnGfNVdk8= +go.opentelemetry.io/collector/pdata v1.44.0/go.mod h1:LnsjYysFc3AwMVh6KGNlkGKJUF2ReuWxtD9Hb3lSMZk= go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/9dNZ0rv454goA= go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= +go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -1647,8 +1665,8 @@ go.opentelemetry.io/otel/exporters/prometheus v0.59.0 h1:HHf+wKS6o5++XZhS98wvILr go.opentelemetry.io/otel/exporters/prometheus v0.59.0/go.mod h1:R8GpRXTZrqvXHDEGVH5bF6+JqAZcK8PjJcZ5nGhEWiE= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJhSENRg+Vvx+ynNilV8twBLBsXMY= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= @@ -1692,10 +1710,10 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= -gocloud.dev v0.42.0 h1:qzG+9ItUL3RPB62/Amugws28n+4vGZXEoJEAMfjutzw= -gocloud.dev v0.42.0/go.mod h1:zkaYAapZfQisXOA4bzhsbA4ckiStGQ3Psvs9/OQ5dPM= -gocloud.dev/secrets/hashivault v0.42.0 h1:kPWIIu1AP6ApHf7HwUrzGrwyRJEfTd46JNUNgnbpsFA= -gocloud.dev/secrets/hashivault v0.42.0/go.mod h1:LXprr1XLEAT7BVZ+Y66dJEHQMzDsowIExj5Ktr9HLvM= +gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M= +gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4= +gocloud.dev/secrets/hashivault v0.43.0 h1:A966rEMpCRUE9209/+k+A2HP2v2qDnrxGpQn+nIH5uY= +gocloud.dev/secrets/hashivault v0.43.0/go.mod h1:KdWKL+TXDi0cXgEd/MTeaidKlotvyJtnTDi71B3rR9U= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1710,8 +1728,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1753,8 +1771,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1809,8 +1827,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1874,7 +1892,6 @@ golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1944,12 +1961,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1964,8 +1981,8 @@ golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -2027,8 +2044,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2089,8 +2106,8 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= -google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= -google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= +google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2180,8 +2197,8 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= +google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= @@ -2251,8 +2268,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= -gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -2351,5 +2366,5 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099Yo sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= -xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= +xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= +xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= diff --git a/apps/iam/kinds/externalgroupmapping.cue b/apps/iam/kinds/externalgroupmapping.cue new file mode 100644 index 00000000000..b4a8bf2d7f5 --- /dev/null +++ b/apps/iam/kinds/externalgroupmapping.cue @@ -0,0 +1,20 @@ +package kinds + +import ( + "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" +) + +externalGroupMappingKind: { + kind: "ExternalGroupMapping" + pluralName: "ExternalGroupMappings" + codegen: { + ts: {enabled: false} + go: {enabled: true} + } +} + +externalGroupMappingv0alpha1: externalGroupMappingKind & { + schema: { + spec: v0alpha1.ExternalGroupMappingSpec + } +} diff --git a/apps/iam/kinds/manifest.cue b/apps/iam/kinds/manifest.cue index c9390acbf3e..bc9c3d47ad7 100644 --- a/apps/iam/kinds/manifest.cue +++ b/apps/iam/kinds/manifest.cue @@ -20,5 +20,6 @@ v0alpha1: { teamv0alpha1, teambindingv0alpha1, serviceaccountv0alpha1, + externalGroupMappingv0alpha1 ] } diff --git a/apps/iam/kinds/v0alpha1/externalgroupmappingspec.cue b/apps/iam/kinds/v0alpha1/externalgroupmappingspec.cue new file mode 100644 index 00000000000..175da727409 --- /dev/null +++ b/apps/iam/kinds/v0alpha1/externalgroupmappingspec.cue @@ -0,0 +1,6 @@ +package v0alpha1 + +ExternalGroupMappingSpec: { + teamRef: TeamRef + externalGroupId: string +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_client_gen.go new file mode 100644 index 00000000000..8419fce6046 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_client_gen.go @@ -0,0 +1,80 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" +) + +type ExternalGroupMappingClient struct { + client *resource.TypedClient[*ExternalGroupMapping, *ExternalGroupMappingList] +} + +func NewExternalGroupMappingClient(client resource.Client) *ExternalGroupMappingClient { + return &ExternalGroupMappingClient{ + client: resource.NewTypedClient[*ExternalGroupMapping, *ExternalGroupMappingList](client, ExternalGroupMappingKind()), + } +} + +func NewExternalGroupMappingClientFromGenerator(generator resource.ClientGenerator) (*ExternalGroupMappingClient, error) { + c, err := generator.ClientFor(ExternalGroupMappingKind()) + if err != nil { + return nil, err + } + return NewExternalGroupMappingClient(c), nil +} + +func (c *ExternalGroupMappingClient) Get(ctx context.Context, identifier resource.Identifier) (*ExternalGroupMapping, error) { + return c.client.Get(ctx, identifier) +} + +func (c *ExternalGroupMappingClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*ExternalGroupMappingList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *ExternalGroupMappingClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*ExternalGroupMappingList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *ExternalGroupMappingClient) Create(ctx context.Context, obj *ExternalGroupMapping, opts resource.CreateOptions) (*ExternalGroupMapping, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = ExternalGroupMappingKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *ExternalGroupMappingClient) Update(ctx context.Context, obj *ExternalGroupMapping, opts resource.UpdateOptions) (*ExternalGroupMapping, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *ExternalGroupMappingClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*ExternalGroupMapping, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *ExternalGroupMappingClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_codec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_codec_gen.go new file mode 100644 index 00000000000..9e519d40b6f --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// ExternalGroupMappingJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type ExternalGroupMappingJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*ExternalGroupMappingJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*ExternalGroupMappingJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &ExternalGroupMappingJSONCodec{} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_metadata_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_metadata_gen.go new file mode 100644 index 00000000000..5b33b2d2279 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type ExternalGroupMappingMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewExternalGroupMappingMetadata creates a new ExternalGroupMappingMetadata object. +func NewExternalGroupMappingMetadata() *ExternalGroupMappingMetadata { + return &ExternalGroupMappingMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go new file mode 100644 index 00000000000..db20616c355 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go @@ -0,0 +1,293 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type ExternalGroupMapping struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the ExternalGroupMapping + Spec ExternalGroupMappingSpec `json:"spec" yaml:"spec"` +} + +func (o *ExternalGroupMapping) GetSpec() any { + return o.Spec +} + +func (o *ExternalGroupMapping) SetSpec(spec any) error { + cast, ok := spec.(ExternalGroupMappingSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *ExternalGroupMapping) GetSubresources() map[string]any { + return map[string]any{} +} + +func (o *ExternalGroupMapping) GetSubresource(name string) (any, bool) { + switch name { + default: + return nil, false + } +} + +func (o *ExternalGroupMapping) SetSubresource(name string, value any) error { + switch name { + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *ExternalGroupMapping) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *ExternalGroupMapping) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *ExternalGroupMapping) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *ExternalGroupMapping) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *ExternalGroupMapping) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *ExternalGroupMapping) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *ExternalGroupMapping) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *ExternalGroupMapping) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *ExternalGroupMapping) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *ExternalGroupMapping) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *ExternalGroupMapping) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *ExternalGroupMapping) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ExternalGroupMapping) DeepCopy() *ExternalGroupMapping { + cpy := &ExternalGroupMapping{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *ExternalGroupMapping) DeepCopyInto(dst *ExternalGroupMapping) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) +} + +// Interface compliance compile-time check +var _ resource.Object = &ExternalGroupMapping{} + +// +k8s:openapi-gen=true +type ExternalGroupMappingList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []ExternalGroupMapping `json:"items" yaml:"items"` +} + +func (o *ExternalGroupMappingList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ExternalGroupMappingList) Copy() resource.ListObject { + cpy := &ExternalGroupMappingList{ + TypeMeta: o.TypeMeta, + Items: make([]ExternalGroupMapping, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*ExternalGroupMapping); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *ExternalGroupMappingList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *ExternalGroupMappingList) SetItems(items []resource.Object) { + o.Items = make([]ExternalGroupMapping, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*ExternalGroupMapping) + } +} + +func (o *ExternalGroupMappingList) DeepCopy() *ExternalGroupMappingList { + cpy := &ExternalGroupMappingList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *ExternalGroupMappingList) DeepCopyInto(dst *ExternalGroupMappingList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &ExternalGroupMappingList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *ExternalGroupMappingSpec) DeepCopy() *ExternalGroupMappingSpec { + cpy := &ExternalGroupMappingSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *ExternalGroupMappingSpec) DeepCopyInto(dst *ExternalGroupMappingSpec) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go new file mode 100644 index 00000000000..91090a9b460 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaExternalGroupMapping = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &ExternalGroupMapping{}, &ExternalGroupMappingList{}, resource.WithKind("ExternalGroupMapping"), + resource.WithPlural("externalgroupmappings"), resource.WithScope(resource.NamespacedScope)) + kindExternalGroupMapping = resource.Kind{ + Schema: schemaExternalGroupMapping, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &ExternalGroupMappingJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func ExternalGroupMappingKind() resource.Kind { + return kindExternalGroupMapping +} + +// Schema returns a resource.SimpleSchema representation of ExternalGroupMapping +func ExternalGroupMappingSchema() *resource.SimpleSchema { + return schemaExternalGroupMapping +} + +// Interface compliance checks +var _ resource.Schema = kindExternalGroupMapping diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_spec_gen.go new file mode 100644 index 00000000000..8bfbfe5b04a --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_spec_gen.go @@ -0,0 +1,27 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type ExternalGroupMappingTeamRef struct { + // Name is the unique identifier for a team. + Name string `json:"name"` +} + +// NewExternalGroupMappingTeamRef creates a new ExternalGroupMappingTeamRef object. +func NewExternalGroupMappingTeamRef() *ExternalGroupMappingTeamRef { + return &ExternalGroupMappingTeamRef{} +} + +// +k8s:openapi-gen=true +type ExternalGroupMappingSpec struct { + TeamRef ExternalGroupMappingTeamRef `json:"teamRef"` + ExternalGroupId string `json:"externalGroupId"` +} + +// NewExternalGroupMappingSpec creates a new ExternalGroupMappingSpec object. +func NewExternalGroupMappingSpec() *ExternalGroupMappingSpec { + return &ExternalGroupMappingSpec{ + TeamRef: *NewExternalGroupMappingTeamRef(), + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/register.go b/apps/iam/pkg/apis/iam/v0alpha1/register.go index 16ee89847ef..54d60c7bd6f 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/register.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/register.go @@ -206,6 +206,30 @@ var TeamBindingResourceInfo = utils.NewResourceInfo( }, ) +var ExternalGroupMappingResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "externalgroupmappings", "externalgroupmapping", "ExternalGroupMapping", + func() runtime.Object { return &ExternalGroupMapping{} }, + func() runtime.Object { return &ExternalGroupMappingList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + mapping, ok := obj.(*ExternalGroupMapping) + if ok { + if mapping != nil { + return []interface{}{ + mapping.Name, + mapping.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + } + return nil, fmt.Errorf("expected external group mapping") + }, + }, +) + var RoleBindingInfo = utils.NewResourceInfo(GROUP, VERSION, "rolebindings", "rolebinding", "RoleBinding", func() runtime.Object { return &RoleBinding{} }, @@ -295,6 +319,8 @@ func AddAuthNKnownTypes(scheme *runtime.Scheme) error { &TeamList{}, &TeamBinding{}, &TeamBindingList{}, + &ExternalGroupMapping{}, + &ExternalGroupMappingList{}, // For now these are registered in pkg/apis/iam/v0alpha1/register.go // &UserTeamList{}, // &ServiceAccountTokenList{}, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index ac03b342c18..f0c517afd38 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -18,6 +18,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleStatus": schema_pkg_apis_iam_v0alpha1_CoreRoleStatus(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolespecPermission": schema_pkg_apis_iam_v0alpha1_CoreRolespecPermission(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_CoreRolestatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_ExternalGroupMapping(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingList": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingSpec": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref), @@ -348,6 +352,145 @@ func schema_pkg_apis_iam_v0alpha1_CoreRolestatusOperatorState(ref common.Referen } } +func schema_pkg_apis_iam_v0alpha1_ExternalGroupMapping(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the ExternalGroupMapping", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMapping"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMapping", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "teamRef": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef"), + }, + }, + "externalGroupId": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"teamRef", "externalGroupId"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is the unique identifier for a team.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index 644ad2ea2d1..388c0dca609 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -96,6 +96,13 @@ var appManifestData = app.ManifestData{ Scope: "Namespaced", Conversion: false, }, + + { + Kind: "ExternalGroupMapping", + Plural: "ExternalGroupMappings", + Scope: "Namespaced", + Conversion: false, + }, }, Routes: app.ManifestVersionRoutes{ Namespaced: map[string]spec3.PathProps{}, @@ -115,16 +122,17 @@ func RemoteManifest() app.Manifest { } var kindVersionToGoType = map[string]resource.Kind{ - "GlobalRole/v0alpha1": v0alpha1.GlobalRoleKind(), - "GlobalRoleBinding/v0alpha1": v0alpha1.GlobalRoleBindingKind(), - "CoreRole/v0alpha1": v0alpha1.CoreRoleKind(), - "Role/v0alpha1": v0alpha1.RoleKind(), - "RoleBinding/v0alpha1": v0alpha1.RoleBindingKind(), - "ResourcePermission/v0alpha1": v0alpha1.ResourcePermissionKind(), - "User/v0alpha1": v0alpha1.UserKind(), - "Team/v0alpha1": v0alpha1.TeamKind(), - "TeamBinding/v0alpha1": v0alpha1.TeamBindingKind(), - "ServiceAccount/v0alpha1": v0alpha1.ServiceAccountKind(), + "GlobalRole/v0alpha1": v0alpha1.GlobalRoleKind(), + "GlobalRoleBinding/v0alpha1": v0alpha1.GlobalRoleBindingKind(), + "CoreRole/v0alpha1": v0alpha1.CoreRoleKind(), + "Role/v0alpha1": v0alpha1.RoleKind(), + "RoleBinding/v0alpha1": v0alpha1.RoleBindingKind(), + "ResourcePermission/v0alpha1": v0alpha1.ResourcePermissionKind(), + "User/v0alpha1": v0alpha1.UserKind(), + "Team/v0alpha1": v0alpha1.TeamKind(), + "TeamBinding/v0alpha1": v0alpha1.TeamBindingKind(), + "ServiceAccount/v0alpha1": v0alpha1.ServiceAccountKind(), + "ExternalGroupMapping/v0alpha1": v0alpha1.ExternalGroupMappingKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/iam/pkg/reconcilers/folder_reconciler.go b/apps/iam/pkg/reconcilers/folder_reconciler.go index 59385b3b3a3..066637ee1e6 100644 --- a/apps/iam/pkg/reconcilers/folder_reconciler.go +++ b/apps/iam/pkg/reconcilers/folder_reconciler.go @@ -5,15 +5,16 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/operator" foldersKind "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/services/authz" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" ) // PermissionStore interface for managing folder permissions @@ -36,7 +37,7 @@ type FolderReconciler struct { func NewFolderReconciler(cfg ReconcilerConfig) (operator.Reconciler, error) { // Create Zanzana client - zanzanaClient, err := authz.NewZanzanaClient("*", cfg.ZanzanaCfg) + zanzanaClient, err := authz.NewRemoteZanzanaClient("*", cfg.ZanzanaCfg) if err != nil { return nil, fmt.Errorf("unable to create zanzana client: %w", err) diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 11e545fb631..fdf11d0b25a 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 + github.com/grafana/grafana-app-sdk v0.48.1 k8s.io/apimachinery v0.34.1 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -31,7 +31,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.46.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -68,13 +68,13 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index fe5ba699151..a0e59cd5731 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -163,8 +163,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -177,20 +177,20 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index 4d034b73343..6d2c5654233 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -5,8 +5,8 @@ go 1.24.0 toolchain go1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.47.0 - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 k8s.io/apimachinery v0.34.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -69,13 +69,13 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index fe5ba699151..a0e59cd5731 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -163,8 +163,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -177,20 +177,20 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index eb2d0319990..387d8ccd445 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 + github.com/grafana/grafana-app-sdk v0.48.1 k8s.io/apimachinery v0.34.1 k8s.io/client-go v0.34.1 k8s.io/klog/v2 v2.130.1 @@ -32,7 +32,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.46.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -69,13 +69,13 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index fe5ba699151..a0e59cd5731 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -163,8 +163,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -177,20 +177,20 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index 4bb9b755dca..e4a419d9214 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -3,9 +3,10 @@ module github.com/grafana/grafana/apps/plugins go 1.25.3 require ( - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 - github.com/grafana/grafana-app-sdk v0.47.0 + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde + github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.1 k8s.io/apiserver v0.34.1 k8s.io/klog/v2 v2.130.1 @@ -14,7 +15,6 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -37,8 +37,8 @@ require ( github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect + github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk/logging v0.46.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -75,14 +75,14 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect @@ -93,7 +93,6 @@ require ( k8s.io/api v0.34.1 // indirect k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/client-go v0.34.1 // indirect - k8s.io/component-base v0.34.1 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index caf9fddb9b7..fb208331835 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -1,7 +1,5 @@ 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -58,10 +56,10 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde h1:ydSrBIOCxJQ84+JU+cyYsOLL40QeXrB7rYfsY/ezU4w= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde/go.mod h1:3MwgP0ISxGviTy3ZUJZsNz/56NNtHztMlH+gcxDt6Tw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -171,16 +169,16 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -193,20 +191,20 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -242,8 +240,6 @@ k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= -k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/apps/plugins/kinds/manifest.cue b/apps/plugins/kinds/manifest.cue index f16ba2d1a72..b3f5251432b 100644 --- a/apps/plugins/kinds/manifest.cue +++ b/apps/plugins/kinds/manifest.cue @@ -5,14 +5,13 @@ manifest: { groupOverride: "plugins.grafana.app" versions: { "v0alpha1": { - served: false + served: true codegen: { ts: {enabled: false} go: {enabled: true} } kinds: [ - pluginMetaV0Alpha1, - pluginInstallV0Alpha1, + pluginV0Alpha1, ] } } diff --git a/apps/plugins/kinds/pluginmeta.cue b/apps/plugins/kinds/plugin.cue similarity index 92% rename from apps/plugins/kinds/pluginmeta.cue rename to apps/plugins/kinds/plugin.cue index 93f41095707..4f381082969 100644 --- a/apps/plugins/kinds/pluginmeta.cue +++ b/apps/plugins/kinds/plugin.cue @@ -4,17 +4,33 @@ import ( "time" ) -pluginMetaV0Alpha1: { - kind: "PluginMeta" - plural: "pluginsmeta" +pluginV0Alpha1: { + kind: "Plugin" + plural: "plugins" scope: "Namespaced" schema: { spec: { - pluginJSON: #JSONData, + id: string + version: string + url?: string + class: "core" | "external" | "cdn" + } + } + routes: { + "/meta": { + "GET": { + request: {} + response: #JSONData, + responseMetadata: { + typeMeta: false + objectMeta: false + } + } } } } + // JSON configuration schema for Grafana plugins // Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json #JSONData: { @@ -217,4 +233,4 @@ pluginMetaV0Alpha1: { title?: string description?: string }] -} +} \ No newline at end of file diff --git a/apps/plugins/kinds/plugininstall.cue b/apps/plugins/kinds/plugininstall.cue deleted file mode 100644 index d72cfac7342..00000000000 --- a/apps/plugins/kinds/plugininstall.cue +++ /dev/null @@ -1,15 +0,0 @@ -package plugins - -pluginInstallV0Alpha1: { - kind: "PluginInstall" - plural: "plugininstalls" - scope: "Namespaced" - schema: { - spec: { - id: string - version: string - url?: string - class: "core" | "external" | "cdn" - } - } -} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_client_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_client_gen.go new file mode 100644 index 00000000000..3e9bcb3a2bb --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_client_gen.go @@ -0,0 +1,123 @@ +package v0alpha1 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type PluginClient struct { + client *resource.TypedClient[*Plugin, *PluginList] +} + +func NewPluginClient(client resource.Client) *PluginClient { + return &PluginClient{ + client: resource.NewTypedClient[*Plugin, *PluginList](client, PluginKind()), + } +} + +func NewPluginClientFromGenerator(generator resource.ClientGenerator) (*PluginClient, error) { + c, err := generator.ClientFor(PluginKind()) + if err != nil { + return nil, err + } + return NewPluginClient(c), nil +} + +func (c *PluginClient) Get(ctx context.Context, identifier resource.Identifier) (*Plugin, error) { + return c.client.Get(ctx, identifier) +} + +func (c *PluginClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *PluginClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *PluginClient) Create(ctx context.Context, obj *Plugin, opts resource.CreateOptions) (*Plugin, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = PluginKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *PluginClient) Update(ctx context.Context, obj *Plugin, opts resource.UpdateOptions) (*Plugin, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *PluginClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Plugin, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *PluginClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus PluginStatus, opts resource.UpdateOptions) (*Plugin, error) { + return c.client.Update(ctx, &Plugin{ + TypeMeta: metav1.TypeMeta{ + Kind: PluginKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *PluginClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} + +type GetMetaRequest struct { + Headers http.Header +} + +func (c *PluginClient) GetMeta(ctx context.Context, identifier resource.Identifier, request GetMetaRequest) (*GetMeta, error) { + resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{ + Path: "/meta", + Verb: "GET", + Headers: request.Headers, + }) + if err != nil { + return nil, err + } + cast := GetMeta{} + err = json.Unmarshal(resp, &cast) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal response bytes into GetMeta: %w", err) + } + return &cast, nil +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_codec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_codec_gen.go new file mode 100644 index 00000000000..c8fe78b00f1 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// PluginJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type PluginJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*PluginJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*PluginJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &PluginJSONCodec{} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_getmeta_response_types_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_getmeta_response_types_gen.go new file mode 100644 index 00000000000..6b0e6899a5b --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_getmeta_response_types_gen.go @@ -0,0 +1,463 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// +k8s:openapi-gen=true +type Info struct { + // Required fields + // +listType=set + Keywords []string `json:"keywords"` + Logos V0alpha1InfoLogos `json:"logos"` + Updated time.Time `json:"updated"` + Version string `json:"version"` + // Optional fields + Author *V0alpha1InfoAuthor `json:"author,omitempty"` + Description *string `json:"description,omitempty"` + // +listType=atomic + Links []V0alpha1InfoLinks `json:"links,omitempty"` + // +listType=atomic + Screenshots []V0alpha1InfoScreenshots `json:"screenshots,omitempty"` +} + +// NewInfo creates a new Info object. +func NewInfo() *Info { + return &Info{ + Keywords: []string{}, + Logos: *NewV0alpha1InfoLogos(), + } +} + +// +k8s:openapi-gen=true +type Dependencies struct { + // Required field + GrafanaDependency string `json:"grafanaDependency"` + // Optional fields + GrafanaVersion *string `json:"grafanaVersion,omitempty"` + // +listType=set + // +listMapKey=id + Plugins []V0alpha1DependenciesPlugins `json:"plugins,omitempty"` + Extensions *V0alpha1DependenciesExtensions `json:"extensions,omitempty"` +} + +// NewDependencies creates a new Dependencies object. +func NewDependencies() *Dependencies { + return &Dependencies{} +} + +// +k8s:openapi-gen=true +type EnterpriseFeatures struct { + // Allow additional properties + HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"` +} + +// NewEnterpriseFeatures creates a new EnterpriseFeatures object. +func NewEnterpriseFeatures() *EnterpriseFeatures { + return &EnterpriseFeatures{ + HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false), + } +} + +// +k8s:openapi-gen=true +type Include struct { + Uid *string `json:"uid,omitempty"` + Type *IncludeType `json:"type,omitempty"` + Name *string `json:"name,omitempty"` + Component *string `json:"component,omitempty"` + Role *IncludeRole `json:"role,omitempty"` + Action *string `json:"action,omitempty"` + Path *string `json:"path,omitempty"` + AddToNav *bool `json:"addToNav,omitempty"` + DefaultNav *bool `json:"defaultNav,omitempty"` + Icon *string `json:"icon,omitempty"` +} + +// NewInclude creates a new Include object. +func NewInclude() *Include { + return &Include{} +} + +// +k8s:openapi-gen=true +type QueryOptions struct { + MaxDataPoints *bool `json:"maxDataPoints,omitempty"` + MinInterval *bool `json:"minInterval,omitempty"` + CacheTimeout *bool `json:"cacheTimeout,omitempty"` +} + +// NewQueryOptions creates a new QueryOptions object. +func NewQueryOptions() *QueryOptions { + return &QueryOptions{} +} + +// +k8s:openapi-gen=true +type Route struct { + Path *string `json:"path,omitempty"` + Method *string `json:"method,omitempty"` + Url *string `json:"url,omitempty"` + ReqSignedIn *bool `json:"reqSignedIn,omitempty"` + ReqRole *string `json:"reqRole,omitempty"` + ReqAction *string `json:"reqAction,omitempty"` + // +listType=atomic + Headers []string `json:"headers,omitempty"` + Body map[string]interface{} `json:"body,omitempty"` + TokenAuth *V0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"` + JwtTokenAuth *V0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"` + // +listType=atomic + UrlParams []V0alpha1RouteUrlParams `json:"urlParams,omitempty"` +} + +// NewRoute creates a new Route object. +func NewRoute() *Route { + return &Route{} +} + +// +k8s:openapi-gen=true +type IAM struct { + // +listType=atomic + Permissions []V0alpha1IAMPermissions `json:"permissions,omitempty"` +} + +// NewIAM creates a new IAM object. +func NewIAM() *IAM { + return &IAM{} +} + +// +k8s:openapi-gen=true +type Role struct { + Role *V0alpha1RoleRole `json:"role,omitempty"` + // +listType=set + Grants []string `json:"grants,omitempty"` +} + +// NewRole creates a new Role object. +func NewRole() *Role { + return &Role{} +} + +// +k8s:openapi-gen=true +type Extensions struct { + // +listType=atomic + AddedComponents []V0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"` + // +listType=atomic + AddedLinks []V0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"` + // +listType=set + // +listMapKey=id + ExposedComponents []V0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"` + // +listType=set + // +listMapKey=id + ExtensionPoints []V0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"` +} + +// NewExtensions creates a new Extensions object. +func NewExtensions() *Extensions { + return &Extensions{} +} + +// +k8s:openapi-gen=true +type GetMeta struct { + // Unique name of the plugin + Id string `json:"id"` + // Plugin type + Type GetMetaType `json:"type"` + // Human-readable name of the plugin + Name string `json:"name"` + // Metadata for the plugin + Info Info `json:"info"` + // Dependency information + Dependencies Dependencies `json:"dependencies"` + // Optional fields + Alerting *bool `json:"alerting,omitempty"` + Annotations *bool `json:"annotations,omitempty"` + AutoEnabled *bool `json:"autoEnabled,omitempty"` + Backend *bool `json:"backend,omitempty"` + BuildMode *string `json:"buildMode,omitempty"` + BuiltIn *bool `json:"builtIn,omitempty"` + Category *GetMetaCategory `json:"category,omitempty"` + EnterpriseFeatures *EnterpriseFeatures `json:"enterpriseFeatures,omitempty"` + Executable *string `json:"executable,omitempty"` + HideFromList *bool `json:"hideFromList,omitempty"` + // +listType=atomic + Includes []Include `json:"includes,omitempty"` + Logs *bool `json:"logs,omitempty"` + Metrics *bool `json:"metrics,omitempty"` + MultiValueFilterOperators *bool `json:"multiValueFilterOperators,omitempty"` + PascalName *string `json:"pascalName,omitempty"` + Preload *bool `json:"preload,omitempty"` + QueryOptions *QueryOptions `json:"queryOptions,omitempty"` + // +listType=atomic + Routes []Route `json:"routes,omitempty"` + SkipDataQuery *bool `json:"skipDataQuery,omitempty"` + State *GetMetaState `json:"state,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + Tracing *bool `json:"tracing,omitempty"` + Iam *IAM `json:"iam,omitempty"` + // +listType=atomic + Roles []Role `json:"roles,omitempty"` + Extensions *Extensions `json:"extensions,omitempty"` +} + +// NewGetMeta creates a new GetMeta object. +func NewGetMeta() *GetMeta { + return &GetMeta{ + Info: *NewInfo(), + Dependencies: *NewDependencies(), + } +} + +// +k8s:openapi-gen=true +type V0alpha1InfoLogos struct { + Small string `json:"small"` + Large string `json:"large"` +} + +// NewV0alpha1InfoLogos creates a new V0alpha1InfoLogos object. +func NewV0alpha1InfoLogos() *V0alpha1InfoLogos { + return &V0alpha1InfoLogos{} +} + +// +k8s:openapi-gen=true +type V0alpha1InfoAuthor struct { + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Url *string `json:"url,omitempty"` +} + +// NewV0alpha1InfoAuthor creates a new V0alpha1InfoAuthor object. +func NewV0alpha1InfoAuthor() *V0alpha1InfoAuthor { + return &V0alpha1InfoAuthor{} +} + +// +k8s:openapi-gen=true +type V0alpha1InfoLinks struct { + Name *string `json:"name,omitempty"` + Url *string `json:"url,omitempty"` +} + +// NewV0alpha1InfoLinks creates a new V0alpha1InfoLinks object. +func NewV0alpha1InfoLinks() *V0alpha1InfoLinks { + return &V0alpha1InfoLinks{} +} + +// +k8s:openapi-gen=true +type V0alpha1InfoScreenshots struct { + Name *string `json:"name,omitempty"` + Path *string `json:"path,omitempty"` +} + +// NewV0alpha1InfoScreenshots creates a new V0alpha1InfoScreenshots object. +func NewV0alpha1InfoScreenshots() *V0alpha1InfoScreenshots { + return &V0alpha1InfoScreenshots{} +} + +// +k8s:openapi-gen=true +type V0alpha1DependenciesPlugins struct { + Id string `json:"id"` + Type V0alpha1DependenciesPluginsType `json:"type"` + Name string `json:"name"` +} + +// NewV0alpha1DependenciesPlugins creates a new V0alpha1DependenciesPlugins object. +func NewV0alpha1DependenciesPlugins() *V0alpha1DependenciesPlugins { + return &V0alpha1DependenciesPlugins{} +} + +// +k8s:openapi-gen=true +type V0alpha1DependenciesExtensions struct { + // +listType=set + ExposedComponents []string `json:"exposedComponents,omitempty"` +} + +// NewV0alpha1DependenciesExtensions creates a new V0alpha1DependenciesExtensions object. +func NewV0alpha1DependenciesExtensions() *V0alpha1DependenciesExtensions { + return &V0alpha1DependenciesExtensions{} +} + +// +k8s:openapi-gen=true +type V0alpha1RouteTokenAuth struct { + Url *string `json:"url,omitempty"` + // +listType=set + Scopes []string `json:"scopes,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// NewV0alpha1RouteTokenAuth creates a new V0alpha1RouteTokenAuth object. +func NewV0alpha1RouteTokenAuth() *V0alpha1RouteTokenAuth { + return &V0alpha1RouteTokenAuth{} +} + +// +k8s:openapi-gen=true +type V0alpha1RouteJwtTokenAuth struct { + Url *string `json:"url,omitempty"` + // +listType=set + Scopes []string `json:"scopes,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// NewV0alpha1RouteJwtTokenAuth creates a new V0alpha1RouteJwtTokenAuth object. +func NewV0alpha1RouteJwtTokenAuth() *V0alpha1RouteJwtTokenAuth { + return &V0alpha1RouteJwtTokenAuth{} +} + +// +k8s:openapi-gen=true +type V0alpha1RouteUrlParams struct { + Name *string `json:"name,omitempty"` + Content *string `json:"content,omitempty"` +} + +// NewV0alpha1RouteUrlParams creates a new V0alpha1RouteUrlParams object. +func NewV0alpha1RouteUrlParams() *V0alpha1RouteUrlParams { + return &V0alpha1RouteUrlParams{} +} + +// +k8s:openapi-gen=true +type V0alpha1IAMPermissions struct { + Action *string `json:"action,omitempty"` + Scope *string `json:"scope,omitempty"` +} + +// NewV0alpha1IAMPermissions creates a new V0alpha1IAMPermissions object. +func NewV0alpha1IAMPermissions() *V0alpha1IAMPermissions { + return &V0alpha1IAMPermissions{} +} + +// +k8s:openapi-gen=true +type V0alpha1RoleRolePermissions struct { + Action *string `json:"action,omitempty"` + Scope *string `json:"scope,omitempty"` +} + +// NewV0alpha1RoleRolePermissions creates a new V0alpha1RoleRolePermissions object. +func NewV0alpha1RoleRolePermissions() *V0alpha1RoleRolePermissions { + return &V0alpha1RoleRolePermissions{} +} + +// +k8s:openapi-gen=true +type V0alpha1RoleRole struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + // +listType=atomic + Permissions []V0alpha1RoleRolePermissions `json:"permissions,omitempty"` +} + +// NewV0alpha1RoleRole creates a new V0alpha1RoleRole object. +func NewV0alpha1RoleRole() *V0alpha1RoleRole { + return &V0alpha1RoleRole{} +} + +// +k8s:openapi-gen=true +type V0alpha1ExtensionsAddedComponents struct { + // +listType=set + Targets []string `json:"targets"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +// NewV0alpha1ExtensionsAddedComponents creates a new V0alpha1ExtensionsAddedComponents object. +func NewV0alpha1ExtensionsAddedComponents() *V0alpha1ExtensionsAddedComponents { + return &V0alpha1ExtensionsAddedComponents{ + Targets: []string{}, + } +} + +// +k8s:openapi-gen=true +type V0alpha1ExtensionsAddedLinks struct { + // +listType=set + Targets []string `json:"targets"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +// NewV0alpha1ExtensionsAddedLinks creates a new V0alpha1ExtensionsAddedLinks object. +func NewV0alpha1ExtensionsAddedLinks() *V0alpha1ExtensionsAddedLinks { + return &V0alpha1ExtensionsAddedLinks{ + Targets: []string{}, + } +} + +// +k8s:openapi-gen=true +type V0alpha1ExtensionsExposedComponents struct { + Id string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` +} + +// NewV0alpha1ExtensionsExposedComponents creates a new V0alpha1ExtensionsExposedComponents object. +func NewV0alpha1ExtensionsExposedComponents() *V0alpha1ExtensionsExposedComponents { + return &V0alpha1ExtensionsExposedComponents{} +} + +// +k8s:openapi-gen=true +type V0alpha1ExtensionsExtensionPoints struct { + Id string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` +} + +// NewV0alpha1ExtensionsExtensionPoints creates a new V0alpha1ExtensionsExtensionPoints object. +func NewV0alpha1ExtensionsExtensionPoints() *V0alpha1ExtensionsExtensionPoints { + return &V0alpha1ExtensionsExtensionPoints{} +} + +// +k8s:openapi-gen=true +type IncludeType string + +const ( + IncludeTypeDashboard IncludeType = "dashboard" + IncludeTypePage IncludeType = "page" + IncludeTypePanel IncludeType = "panel" + IncludeTypeDatasource IncludeType = "datasource" +) + +// +k8s:openapi-gen=true +type IncludeRole string + +const ( + IncludeRoleAdmin IncludeRole = "Admin" + IncludeRoleEditor IncludeRole = "Editor" + IncludeRoleViewer IncludeRole = "Viewer" +) + +// +k8s:openapi-gen=true +type GetMetaType string + +const ( + GetMetaTypeApp GetMetaType = "app" + GetMetaTypeDatasource GetMetaType = "datasource" + GetMetaTypePanel GetMetaType = "panel" + GetMetaTypeRenderer GetMetaType = "renderer" +) + +// +k8s:openapi-gen=true +type GetMetaCategory string + +const ( + GetMetaCategoryTsdb GetMetaCategory = "tsdb" + GetMetaCategoryLogging GetMetaCategory = "logging" + GetMetaCategoryCloud GetMetaCategory = "cloud" + GetMetaCategoryTracing GetMetaCategory = "tracing" + GetMetaCategoryProfiling GetMetaCategory = "profiling" + GetMetaCategorySql GetMetaCategory = "sql" + GetMetaCategoryEnterprise GetMetaCategory = "enterprise" + GetMetaCategoryIot GetMetaCategory = "iot" + GetMetaCategoryOther GetMetaCategory = "other" +) + +// +k8s:openapi-gen=true +type GetMetaState string + +const ( + GetMetaStateAlpha GetMetaState = "alpha" + GetMetaStateBeta GetMetaState = "beta" +) + +// +k8s:openapi-gen=true +type V0alpha1DependenciesPluginsType string + +const ( + V0alpha1DependenciesPluginsTypeApp V0alpha1DependenciesPluginsType = "app" + V0alpha1DependenciesPluginsTypeDatasource V0alpha1DependenciesPluginsType = "datasource" + V0alpha1DependenciesPluginsTypePanel V0alpha1DependenciesPluginsType = "panel" +) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_metadata_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_metadata_gen.go new file mode 100644 index 00000000000..fe30fe5e614 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type PluginMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewPluginMetadata creates a new PluginMetadata object. +func NewPluginMetadata() *PluginMetadata { + return &PluginMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go similarity index 69% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go index dac431ebf12..d2cdedba399 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go @@ -15,22 +15,22 @@ import ( ) // +k8s:openapi-gen=true -type PluginMeta struct { +type Plugin struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ObjectMeta `json:"metadata" yaml:"metadata"` - // Spec is the spec of the PluginMeta - Spec PluginMetaSpec `json:"spec" yaml:"spec"` + // Spec is the spec of the Plugin + Spec PluginSpec `json:"spec" yaml:"spec"` - Status PluginMetaStatus `json:"status" yaml:"status"` + Status PluginStatus `json:"status" yaml:"status"` } -func (o *PluginMeta) GetSpec() any { +func (o *Plugin) GetSpec() any { return o.Spec } -func (o *PluginMeta) SetSpec(spec any) error { - cast, ok := spec.(PluginMetaSpec) +func (o *Plugin) SetSpec(spec any) error { + cast, ok := spec.(PluginSpec) if !ok { return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) } @@ -38,13 +38,13 @@ func (o *PluginMeta) SetSpec(spec any) error { return nil } -func (o *PluginMeta) GetSubresources() map[string]any { +func (o *Plugin) GetSubresources() map[string]any { return map[string]any{ "status": o.Status, } } -func (o *PluginMeta) GetSubresource(name string) (any, bool) { +func (o *Plugin) GetSubresource(name string) (any, bool) { switch name { case "status": return o.Status, true @@ -53,12 +53,12 @@ func (o *PluginMeta) GetSubresource(name string) (any, bool) { } } -func (o *PluginMeta) SetSubresource(name string, value any) error { +func (o *Plugin) SetSubresource(name string, value any) error { switch name { case "status": - cast, ok := value.(PluginMetaStatus) + cast, ok := value.(PluginStatus) if !ok { - return fmt.Errorf("cannot set status type %#v, not of type PluginMetaStatus", value) + return fmt.Errorf("cannot set status type %#v, not of type PluginStatus", value) } o.Status = cast return nil @@ -67,7 +67,7 @@ func (o *PluginMeta) SetSubresource(name string, value any) error { } } -func (o *PluginMeta) GetStaticMetadata() resource.StaticMetadata { +func (o *Plugin) GetStaticMetadata() resource.StaticMetadata { gvk := o.GroupVersionKind() return resource.StaticMetadata{ Name: o.ObjectMeta.Name, @@ -78,7 +78,7 @@ func (o *PluginMeta) GetStaticMetadata() resource.StaticMetadata { } } -func (o *PluginMeta) SetStaticMetadata(metadata resource.StaticMetadata) { +func (o *Plugin) SetStaticMetadata(metadata resource.StaticMetadata) { o.Name = metadata.Name o.Namespace = metadata.Namespace o.SetGroupVersionKind(schema.GroupVersionKind{ @@ -88,7 +88,7 @@ func (o *PluginMeta) SetStaticMetadata(metadata resource.StaticMetadata) { }) } -func (o *PluginMeta) GetCommonMetadata() resource.CommonMetadata { +func (o *Plugin) GetCommonMetadata() resource.CommonMetadata { dt := o.DeletionTimestamp var deletionTimestamp *time.Time if dt != nil { @@ -120,7 +120,7 @@ func (o *PluginMeta) GetCommonMetadata() resource.CommonMetadata { } } -func (o *PluginMeta) SetCommonMetadata(metadata resource.CommonMetadata) { +func (o *Plugin) SetCommonMetadata(metadata resource.CommonMetadata) { o.UID = types.UID(metadata.UID) o.ResourceVersion = metadata.ResourceVersion o.Generation = metadata.Generation @@ -165,7 +165,7 @@ func (o *PluginMeta) SetCommonMetadata(metadata resource.CommonMetadata) { } } -func (o *PluginMeta) GetCreatedBy() string { +func (o *Plugin) GetCreatedBy() string { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -173,7 +173,7 @@ func (o *PluginMeta) GetCreatedBy() string { return o.ObjectMeta.Annotations["grafana.com/createdBy"] } -func (o *PluginMeta) SetCreatedBy(createdBy string) { +func (o *Plugin) SetCreatedBy(createdBy string) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -181,7 +181,7 @@ func (o *PluginMeta) SetCreatedBy(createdBy string) { o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy } -func (o *PluginMeta) GetUpdateTimestamp() time.Time { +func (o *Plugin) GetUpdateTimestamp() time.Time { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -190,7 +190,7 @@ func (o *PluginMeta) GetUpdateTimestamp() time.Time { return parsed } -func (o *PluginMeta) SetUpdateTimestamp(updateTimestamp time.Time) { +func (o *Plugin) SetUpdateTimestamp(updateTimestamp time.Time) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -198,7 +198,7 @@ func (o *PluginMeta) SetUpdateTimestamp(updateTimestamp time.Time) { o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) } -func (o *PluginMeta) GetUpdatedBy() string { +func (o *Plugin) GetUpdatedBy() string { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -206,7 +206,7 @@ func (o *PluginMeta) GetUpdatedBy() string { return o.ObjectMeta.Annotations["grafana.com/updatedBy"] } -func (o *PluginMeta) SetUpdatedBy(updatedBy string) { +func (o *Plugin) SetUpdatedBy(updatedBy string) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -214,21 +214,21 @@ func (o *PluginMeta) SetUpdatedBy(updatedBy string) { o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy } -func (o *PluginMeta) Copy() resource.Object { +func (o *Plugin) Copy() resource.Object { return resource.CopyObject(o) } -func (o *PluginMeta) DeepCopyObject() runtime.Object { +func (o *Plugin) DeepCopyObject() runtime.Object { return o.Copy() } -func (o *PluginMeta) DeepCopy() *PluginMeta { - cpy := &PluginMeta{} +func (o *Plugin) DeepCopy() *Plugin { + cpy := &Plugin{} o.DeepCopyInto(cpy) return cpy } -func (o *PluginMeta) DeepCopyInto(dst *PluginMeta) { +func (o *Plugin) DeepCopyInto(dst *Plugin) { dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) @@ -237,34 +237,34 @@ func (o *PluginMeta) DeepCopyInto(dst *PluginMeta) { } // Interface compliance compile-time check -var _ resource.Object = &PluginMeta{} +var _ resource.Object = &Plugin{} // +k8s:openapi-gen=true -type PluginMetaList struct { +type PluginList struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ListMeta `json:"metadata" yaml:"metadata"` - Items []PluginMeta `json:"items" yaml:"items"` + Items []Plugin `json:"items" yaml:"items"` } -func (o *PluginMetaList) DeepCopyObject() runtime.Object { +func (o *PluginList) DeepCopyObject() runtime.Object { return o.Copy() } -func (o *PluginMetaList) Copy() resource.ListObject { - cpy := &PluginMetaList{ +func (o *PluginList) Copy() resource.ListObject { + cpy := &PluginList{ TypeMeta: o.TypeMeta, - Items: make([]PluginMeta, len(o.Items)), + Items: make([]Plugin, len(o.Items)), } o.ListMeta.DeepCopyInto(&cpy.ListMeta) for i := 0; i < len(o.Items); i++ { - if item, ok := o.Items[i].Copy().(*PluginMeta); ok { + if item, ok := o.Items[i].Copy().(*Plugin); ok { cpy.Items[i] = *item } } return cpy } -func (o *PluginMetaList) GetItems() []resource.Object { +func (o *PluginList) GetItems() []resource.Object { items := make([]resource.Object, len(o.Items)) for i := 0; i < len(o.Items); i++ { items[i] = &o.Items[i] @@ -272,48 +272,48 @@ func (o *PluginMetaList) GetItems() []resource.Object { return items } -func (o *PluginMetaList) SetItems(items []resource.Object) { - o.Items = make([]PluginMeta, len(items)) +func (o *PluginList) SetItems(items []resource.Object) { + o.Items = make([]Plugin, len(items)) for i := 0; i < len(items); i++ { - o.Items[i] = *items[i].(*PluginMeta) + o.Items[i] = *items[i].(*Plugin) } } -func (o *PluginMetaList) DeepCopy() *PluginMetaList { - cpy := &PluginMetaList{} +func (o *PluginList) DeepCopy() *PluginList { + cpy := &PluginList{} o.DeepCopyInto(cpy) return cpy } -func (o *PluginMetaList) DeepCopyInto(dst *PluginMetaList) { +func (o *PluginList) DeepCopyInto(dst *PluginList) { resource.CopyObjectInto(dst, o) } // Interface compliance compile-time check -var _ resource.ListObject = &PluginMetaList{} +var _ resource.ListObject = &PluginList{} // Copy methods for all subresource types // DeepCopy creates a full deep copy of Spec -func (s *PluginMetaSpec) DeepCopy() *PluginMetaSpec { - cpy := &PluginMetaSpec{} +func (s *PluginSpec) DeepCopy() *PluginSpec { + cpy := &PluginSpec{} s.DeepCopyInto(cpy) return cpy } // DeepCopyInto deep copies Spec into another Spec object -func (s *PluginMetaSpec) DeepCopyInto(dst *PluginMetaSpec) { +func (s *PluginSpec) DeepCopyInto(dst *PluginSpec) { resource.CopyObjectInto(dst, s) } -// DeepCopy creates a full deep copy of PluginMetaStatus -func (s *PluginMetaStatus) DeepCopy() *PluginMetaStatus { - cpy := &PluginMetaStatus{} +// DeepCopy creates a full deep copy of PluginStatus +func (s *PluginStatus) DeepCopy() *PluginStatus { + cpy := &PluginStatus{} s.DeepCopyInto(cpy) return cpy } -// DeepCopyInto deep copies PluginMetaStatus into another PluginMetaStatus object -func (s *PluginMetaStatus) DeepCopyInto(dst *PluginMetaStatus) { +// DeepCopyInto deep copies PluginStatus into another PluginStatus object +func (s *PluginStatus) DeepCopyInto(dst *PluginStatus) { resource.CopyObjectInto(dst, s) } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go new file mode 100644 index 00000000000..97024275a1c --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaPlugin = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", &Plugin{}, &PluginList{}, resource.WithKind("Plugin"), + resource.WithPlural("plugins"), resource.WithScope(resource.NamespacedScope)) + kindPlugin = resource.Kind{ + Schema: schemaPlugin, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &PluginJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func PluginKind() resource.Kind { + return kindPlugin +} + +// Schema returns a resource.SimpleSchema representation of Plugin +func PluginSchema() *resource.SimpleSchema { + return schemaPlugin +} + +// Interface compliance checks +var _ resource.Schema = kindPlugin diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go new file mode 100644 index 00000000000..d46a0a4aed1 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go @@ -0,0 +1,25 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type PluginSpec struct { + Id string `json:"id"` + Version string `json:"version"` + Url *string `json:"url,omitempty"` + Class PluginSpecClass `json:"class"` +} + +// NewPluginSpec creates a new PluginSpec object. +func NewPluginSpec() *PluginSpec { + return &PluginSpec{} +} + +// +k8s:openapi-gen=true +type PluginSpecClass string + +const ( + PluginSpecClassCore PluginSpecClass = "core" + PluginSpecClassExternal PluginSpecClass = "external" + PluginSpecClassCdn PluginSpecClass = "cdn" +) diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_status_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_status_gen.go similarity index 53% rename from apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_status_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/plugin_status_gen.go index b1b12ddfb33..55ecac4408f 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_status_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_status_gen.go @@ -3,42 +3,42 @@ package v0alpha1 // +k8s:openapi-gen=true -type ReceiverstatusOperatorState struct { +type PluginstatusOperatorState struct { // lastEvaluation is the ResourceVersion last evaluated LastEvaluation string `json:"lastEvaluation"` // state describes the state of the lastEvaluation. // It is limited to three possible states for machine evaluation. - State ReceiverStatusOperatorStateState `json:"state"` + State PluginStatusOperatorStateState `json:"state"` // descriptiveState is an optional more descriptive state field which has no requirements on format DescriptiveState *string `json:"descriptiveState,omitempty"` // details contains any extra information that is operator-specific Details map[string]interface{} `json:"details,omitempty"` } -// NewReceiverstatusOperatorState creates a new ReceiverstatusOperatorState object. -func NewReceiverstatusOperatorState() *ReceiverstatusOperatorState { - return &ReceiverstatusOperatorState{} +// NewPluginstatusOperatorState creates a new PluginstatusOperatorState object. +func NewPluginstatusOperatorState() *PluginstatusOperatorState { + return &PluginstatusOperatorState{} } // +k8s:openapi-gen=true -type ReceiverStatus struct { +type PluginStatus struct { // operatorStates is a map of operator ID to operator state evaluations. // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]ReceiverstatusOperatorState `json:"operatorStates,omitempty"` + OperatorStates map[string]PluginstatusOperatorState `json:"operatorStates,omitempty"` // additionalFields is reserved for future use AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` } -// NewReceiverStatus creates a new ReceiverStatus object. -func NewReceiverStatus() *ReceiverStatus { - return &ReceiverStatus{} +// NewPluginStatus creates a new PluginStatus object. +func NewPluginStatus() *PluginStatus { + return &PluginStatus{} } // +k8s:openapi-gen=true -type ReceiverStatusOperatorStateState string +type PluginStatusOperatorStateState string const ( - ReceiverStatusOperatorStateStateSuccess ReceiverStatusOperatorStateState = "success" - ReceiverStatusOperatorStateStateInProgress ReceiverStatusOperatorStateState = "in_progress" - ReceiverStatusOperatorStateStateFailed ReceiverStatusOperatorStateState = "failed" + PluginStatusOperatorStateStateSuccess PluginStatusOperatorStateState = "success" + PluginStatusOperatorStateStateInProgress PluginStatusOperatorStateState = "in_progress" + PluginStatusOperatorStateStateFailed PluginStatusOperatorStateState = "failed" ) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_client_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_client_gen.go deleted file mode 100644 index 7733db871e0..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_client_gen.go +++ /dev/null @@ -1,99 +0,0 @@ -package v0alpha1 - -import ( - "context" - - "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type PluginInstallClient struct { - client *resource.TypedClient[*PluginInstall, *PluginInstallList] -} - -func NewPluginInstallClient(client resource.Client) *PluginInstallClient { - return &PluginInstallClient{ - client: resource.NewTypedClient[*PluginInstall, *PluginInstallList](client, PluginInstallKind()), - } -} - -func NewPluginInstallClientFromGenerator(generator resource.ClientGenerator) (*PluginInstallClient, error) { - c, err := generator.ClientFor(PluginInstallKind()) - if err != nil { - return nil, err - } - return NewPluginInstallClient(c), nil -} - -func (c *PluginInstallClient) Get(ctx context.Context, identifier resource.Identifier) (*PluginInstall, error) { - return c.client.Get(ctx, identifier) -} - -func (c *PluginInstallClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginInstallList, error) { - return c.client.List(ctx, namespace, opts) -} - -func (c *PluginInstallClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginInstallList, error) { - resp, err := c.client.List(ctx, namespace, resource.ListOptions{ - ResourceVersion: opts.ResourceVersion, - Limit: opts.Limit, - LabelFilters: opts.LabelFilters, - FieldSelectors: opts.FieldSelectors, - }) - if err != nil { - return nil, err - } - for resp.GetContinue() != "" { - page, err := c.client.List(ctx, namespace, resource.ListOptions{ - Continue: resp.GetContinue(), - ResourceVersion: opts.ResourceVersion, - Limit: opts.Limit, - LabelFilters: opts.LabelFilters, - FieldSelectors: opts.FieldSelectors, - }) - if err != nil { - return nil, err - } - resp.SetContinue(page.GetContinue()) - resp.SetResourceVersion(page.GetResourceVersion()) - resp.SetItems(append(resp.GetItems(), page.GetItems()...)) - } - return resp, nil -} - -func (c *PluginInstallClient) Create(ctx context.Context, obj *PluginInstall, opts resource.CreateOptions) (*PluginInstall, error) { - // Make sure apiVersion and kind are set - obj.APIVersion = GroupVersion.Identifier() - obj.Kind = PluginInstallKind().Kind() - return c.client.Create(ctx, obj, opts) -} - -func (c *PluginInstallClient) Update(ctx context.Context, obj *PluginInstall, opts resource.UpdateOptions) (*PluginInstall, error) { - return c.client.Update(ctx, obj, opts) -} - -func (c *PluginInstallClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*PluginInstall, error) { - return c.client.Patch(ctx, identifier, req, opts) -} - -func (c *PluginInstallClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus PluginInstallStatus, opts resource.UpdateOptions) (*PluginInstall, error) { - return c.client.Update(ctx, &PluginInstall{ - TypeMeta: metav1.TypeMeta{ - Kind: PluginInstallKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - -func (c *PluginInstallClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { - return c.client.Delete(ctx, identifier, opts) -} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_schema_gen.go deleted file mode 100644 index 85ea40cd157..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_schema_gen.go +++ /dev/null @@ -1,34 +0,0 @@ -// -// Code generated by grafana-app-sdk. DO NOT EDIT. -// - -package v0alpha1 - -import ( - "github.com/grafana/grafana-app-sdk/resource" -) - -// schema is unexported to prevent accidental overwrites -var ( - schemaPluginInstall = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", &PluginInstall{}, &PluginInstallList{}, resource.WithKind("PluginInstall"), - resource.WithPlural("plugininstalls"), resource.WithScope(resource.NamespacedScope)) - kindPluginInstall = resource.Kind{ - Schema: schemaPluginInstall, - Codecs: map[resource.KindEncoding]resource.Codec{ - resource.KindEncodingJSON: &PluginInstallJSONCodec{}, - }, - } -) - -// Kind returns a resource.Kind for this Schema with a JSON codec -func PluginInstallKind() resource.Kind { - return kindPluginInstall -} - -// Schema returns a resource.SimpleSchema representation of PluginInstall -func PluginInstallSchema() *resource.SimpleSchema { - return schemaPluginInstall -} - -// Interface compliance checks -var _ resource.Schema = kindPluginInstall diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_spec_gen.go deleted file mode 100644 index eeccd7f656d..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_spec_gen.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// +k8s:openapi-gen=true -type PluginInstallSpec struct { - Id string `json:"id"` - Version string `json:"version"` - Url *string `json:"url,omitempty"` - Class PluginInstallSpecClass `json:"class"` -} - -// NewPluginInstallSpec creates a new PluginInstallSpec object. -func NewPluginInstallSpec() *PluginInstallSpec { - return &PluginInstallSpec{} -} - -// +k8s:openapi-gen=true -type PluginInstallSpecClass string - -const ( - PluginInstallSpecClassCore PluginInstallSpecClass = "core" - PluginInstallSpecClassExternal PluginInstallSpecClass = "external" - PluginInstallSpecClassCdn PluginInstallSpecClass = "cdn" -) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_status_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_status_gen.go deleted file mode 100644 index 77fc07860b0..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugininstall_status_gen.go +++ /dev/null @@ -1,44 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// +k8s:openapi-gen=true -type PluginInstallstatusOperatorState struct { - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State PluginInstallStatusOperatorStateState `json:"state"` - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` -} - -// NewPluginInstallstatusOperatorState creates a new PluginInstallstatusOperatorState object. -func NewPluginInstallstatusOperatorState() *PluginInstallstatusOperatorState { - return &PluginInstallstatusOperatorState{} -} - -// +k8s:openapi-gen=true -type PluginInstallStatus struct { - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]PluginInstallstatusOperatorState `json:"operatorStates,omitempty"` - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` -} - -// NewPluginInstallStatus creates a new PluginInstallStatus object. -func NewPluginInstallStatus() *PluginInstallStatus { - return &PluginInstallStatus{} -} - -// +k8s:openapi-gen=true -type PluginInstallStatusOperatorStateState string - -const ( - PluginInstallStatusOperatorStateStateSuccess PluginInstallStatusOperatorStateState = "success" - PluginInstallStatusOperatorStateStateInProgress PluginInstallStatusOperatorStateState = "in_progress" - PluginInstallStatusOperatorStateStateFailed PluginInstallStatusOperatorStateState = "failed" -) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go deleted file mode 100644 index a4022c8de97..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go +++ /dev/null @@ -1,34 +0,0 @@ -// -// Code generated by grafana-app-sdk. DO NOT EDIT. -// - -package v0alpha1 - -import ( - "github.com/grafana/grafana-app-sdk/resource" -) - -// schema is unexported to prevent accidental overwrites -var ( - schemaPluginMeta = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", &PluginMeta{}, &PluginMetaList{}, resource.WithKind("PluginMeta"), - resource.WithPlural("pluginmetas"), resource.WithScope(resource.NamespacedScope)) - kindPluginMeta = resource.Kind{ - Schema: schemaPluginMeta, - Codecs: map[resource.KindEncoding]resource.Codec{ - resource.KindEncodingJSON: &PluginMetaJSONCodec{}, - }, - } -) - -// Kind returns a resource.Kind for this Schema with a JSON codec -func PluginMetaKind() resource.Kind { - return kindPluginMeta -} - -// Schema returns a resource.SimpleSchema representation of PluginMeta -func PluginMetaSchema() *resource.SimpleSchema { - return schemaPluginMeta -} - -// Interface compliance checks -var _ resource.Schema = kindPluginMeta diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go deleted file mode 100644 index 8020e34efda..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go +++ /dev/null @@ -1,477 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -import ( - time "time" -) - -// JSON configuration schema for Grafana plugins -// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json -// +k8s:openapi-gen=true -type PluginMetaJSONData struct { - // Unique name of the plugin - Id string `json:"id"` - // Plugin type - Type PluginMetaJSONDataType `json:"type"` - // Human-readable name of the plugin - Name string `json:"name"` - // Metadata for the plugin - Info PluginMetaInfo `json:"info"` - // Dependency information - Dependencies PluginMetaDependencies `json:"dependencies"` - // Optional fields - Alerting *bool `json:"alerting,omitempty"` - Annotations *bool `json:"annotations,omitempty"` - AutoEnabled *bool `json:"autoEnabled,omitempty"` - Backend *bool `json:"backend,omitempty"` - BuildMode *string `json:"buildMode,omitempty"` - BuiltIn *bool `json:"builtIn,omitempty"` - Category *PluginMetaJSONDataCategory `json:"category,omitempty"` - EnterpriseFeatures *PluginMetaEnterpriseFeatures `json:"enterpriseFeatures,omitempty"` - Executable *string `json:"executable,omitempty"` - HideFromList *bool `json:"hideFromList,omitempty"` - // +listType=atomic - Includes []PluginMetaInclude `json:"includes,omitempty"` - Logs *bool `json:"logs,omitempty"` - Metrics *bool `json:"metrics,omitempty"` - MultiValueFilterOperators *bool `json:"multiValueFilterOperators,omitempty"` - PascalName *string `json:"pascalName,omitempty"` - Preload *bool `json:"preload,omitempty"` - QueryOptions *PluginMetaQueryOptions `json:"queryOptions,omitempty"` - // +listType=atomic - Routes []PluginMetaRoute `json:"routes,omitempty"` - SkipDataQuery *bool `json:"skipDataQuery,omitempty"` - State *PluginMetaJSONDataState `json:"state,omitempty"` - Streaming *bool `json:"streaming,omitempty"` - Tracing *bool `json:"tracing,omitempty"` - Iam *PluginMetaIAM `json:"iam,omitempty"` - // +listType=atomic - Roles []PluginMetaRole `json:"roles,omitempty"` - Extensions *PluginMetaExtensions `json:"extensions,omitempty"` -} - -// NewPluginMetaJSONData creates a new PluginMetaJSONData object. -func NewPluginMetaJSONData() *PluginMetaJSONData { - return &PluginMetaJSONData{ - Info: *NewPluginMetaInfo(), - Dependencies: *NewPluginMetaDependencies(), - } -} - -// +k8s:openapi-gen=true -type PluginMetaInfo struct { - // Required fields - // +listType=set - Keywords []string `json:"keywords"` - Logos PluginMetaV0alpha1InfoLogos `json:"logos"` - Updated time.Time `json:"updated"` - Version string `json:"version"` - // Optional fields - Author *PluginMetaV0alpha1InfoAuthor `json:"author,omitempty"` - Description *string `json:"description,omitempty"` - // +listType=atomic - Links []PluginMetaV0alpha1InfoLinks `json:"links,omitempty"` - // +listType=atomic - Screenshots []PluginMetaV0alpha1InfoScreenshots `json:"screenshots,omitempty"` -} - -// NewPluginMetaInfo creates a new PluginMetaInfo object. -func NewPluginMetaInfo() *PluginMetaInfo { - return &PluginMetaInfo{ - Keywords: []string{}, - Logos: *NewPluginMetaV0alpha1InfoLogos(), - } -} - -// +k8s:openapi-gen=true -type PluginMetaDependencies struct { - // Required field - GrafanaDependency string `json:"grafanaDependency"` - // Optional fields - GrafanaVersion *string `json:"grafanaVersion,omitempty"` - // +listType=set - // +listMapKey=id - Plugins []PluginMetaV0alpha1DependenciesPlugins `json:"plugins,omitempty"` - Extensions *PluginMetaV0alpha1DependenciesExtensions `json:"extensions,omitempty"` -} - -// NewPluginMetaDependencies creates a new PluginMetaDependencies object. -func NewPluginMetaDependencies() *PluginMetaDependencies { - return &PluginMetaDependencies{} -} - -// +k8s:openapi-gen=true -type PluginMetaEnterpriseFeatures struct { - // Allow additional properties - HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"` -} - -// NewPluginMetaEnterpriseFeatures creates a new PluginMetaEnterpriseFeatures object. -func NewPluginMetaEnterpriseFeatures() *PluginMetaEnterpriseFeatures { - return &PluginMetaEnterpriseFeatures{ - HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false), - } -} - -// +k8s:openapi-gen=true -type PluginMetaInclude struct { - Uid *string `json:"uid,omitempty"` - Type *PluginMetaIncludeType `json:"type,omitempty"` - Name *string `json:"name,omitempty"` - Component *string `json:"component,omitempty"` - Role *PluginMetaIncludeRole `json:"role,omitempty"` - Action *string `json:"action,omitempty"` - Path *string `json:"path,omitempty"` - AddToNav *bool `json:"addToNav,omitempty"` - DefaultNav *bool `json:"defaultNav,omitempty"` - Icon *string `json:"icon,omitempty"` -} - -// NewPluginMetaInclude creates a new PluginMetaInclude object. -func NewPluginMetaInclude() *PluginMetaInclude { - return &PluginMetaInclude{} -} - -// +k8s:openapi-gen=true -type PluginMetaQueryOptions struct { - MaxDataPoints *bool `json:"maxDataPoints,omitempty"` - MinInterval *bool `json:"minInterval,omitempty"` - CacheTimeout *bool `json:"cacheTimeout,omitempty"` -} - -// NewPluginMetaQueryOptions creates a new PluginMetaQueryOptions object. -func NewPluginMetaQueryOptions() *PluginMetaQueryOptions { - return &PluginMetaQueryOptions{} -} - -// +k8s:openapi-gen=true -type PluginMetaRoute struct { - Path *string `json:"path,omitempty"` - Method *string `json:"method,omitempty"` - Url *string `json:"url,omitempty"` - ReqSignedIn *bool `json:"reqSignedIn,omitempty"` - ReqRole *string `json:"reqRole,omitempty"` - ReqAction *string `json:"reqAction,omitempty"` - // +listType=atomic - Headers []string `json:"headers,omitempty"` - Body map[string]interface{} `json:"body,omitempty"` - TokenAuth *PluginMetaV0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"` - JwtTokenAuth *PluginMetaV0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"` - // +listType=atomic - UrlParams []PluginMetaV0alpha1RouteUrlParams `json:"urlParams,omitempty"` -} - -// NewPluginMetaRoute creates a new PluginMetaRoute object. -func NewPluginMetaRoute() *PluginMetaRoute { - return &PluginMetaRoute{} -} - -// +k8s:openapi-gen=true -type PluginMetaIAM struct { - // +listType=atomic - Permissions []PluginMetaV0alpha1IAMPermissions `json:"permissions,omitempty"` -} - -// NewPluginMetaIAM creates a new PluginMetaIAM object. -func NewPluginMetaIAM() *PluginMetaIAM { - return &PluginMetaIAM{} -} - -// +k8s:openapi-gen=true -type PluginMetaRole struct { - Role *PluginMetaV0alpha1RoleRole `json:"role,omitempty"` - // +listType=set - Grants []string `json:"grants,omitempty"` -} - -// NewPluginMetaRole creates a new PluginMetaRole object. -func NewPluginMetaRole() *PluginMetaRole { - return &PluginMetaRole{} -} - -// +k8s:openapi-gen=true -type PluginMetaExtensions struct { - // +listType=atomic - AddedComponents []PluginMetaV0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"` - // +listType=atomic - AddedLinks []PluginMetaV0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"` - // +listType=set - // +listMapKey=id - ExposedComponents []PluginMetaV0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"` - // +listType=set - // +listMapKey=id - ExtensionPoints []PluginMetaV0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"` -} - -// NewPluginMetaExtensions creates a new PluginMetaExtensions object. -func NewPluginMetaExtensions() *PluginMetaExtensions { - return &PluginMetaExtensions{} -} - -// +k8s:openapi-gen=true -type PluginMetaSpec struct { - PluginJSON PluginMetaJSONData `json:"pluginJSON"` -} - -// NewPluginMetaSpec creates a new PluginMetaSpec object. -func NewPluginMetaSpec() *PluginMetaSpec { - return &PluginMetaSpec{ - PluginJSON: *NewPluginMetaJSONData(), - } -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1InfoLogos struct { - Small string `json:"small"` - Large string `json:"large"` -} - -// NewPluginMetaV0alpha1InfoLogos creates a new PluginMetaV0alpha1InfoLogos object. -func NewPluginMetaV0alpha1InfoLogos() *PluginMetaV0alpha1InfoLogos { - return &PluginMetaV0alpha1InfoLogos{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1InfoAuthor struct { - Name *string `json:"name,omitempty"` - Email *string `json:"email,omitempty"` - Url *string `json:"url,omitempty"` -} - -// NewPluginMetaV0alpha1InfoAuthor creates a new PluginMetaV0alpha1InfoAuthor object. -func NewPluginMetaV0alpha1InfoAuthor() *PluginMetaV0alpha1InfoAuthor { - return &PluginMetaV0alpha1InfoAuthor{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1InfoLinks struct { - Name *string `json:"name,omitempty"` - Url *string `json:"url,omitempty"` -} - -// NewPluginMetaV0alpha1InfoLinks creates a new PluginMetaV0alpha1InfoLinks object. -func NewPluginMetaV0alpha1InfoLinks() *PluginMetaV0alpha1InfoLinks { - return &PluginMetaV0alpha1InfoLinks{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1InfoScreenshots struct { - Name *string `json:"name,omitempty"` - Path *string `json:"path,omitempty"` -} - -// NewPluginMetaV0alpha1InfoScreenshots creates a new PluginMetaV0alpha1InfoScreenshots object. -func NewPluginMetaV0alpha1InfoScreenshots() *PluginMetaV0alpha1InfoScreenshots { - return &PluginMetaV0alpha1InfoScreenshots{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1DependenciesPlugins struct { - Id string `json:"id"` - Type PluginMetaV0alpha1DependenciesPluginsType `json:"type"` - Name string `json:"name"` -} - -// NewPluginMetaV0alpha1DependenciesPlugins creates a new PluginMetaV0alpha1DependenciesPlugins object. -func NewPluginMetaV0alpha1DependenciesPlugins() *PluginMetaV0alpha1DependenciesPlugins { - return &PluginMetaV0alpha1DependenciesPlugins{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1DependenciesExtensions struct { - // +listType=set - ExposedComponents []string `json:"exposedComponents,omitempty"` -} - -// NewPluginMetaV0alpha1DependenciesExtensions creates a new PluginMetaV0alpha1DependenciesExtensions object. -func NewPluginMetaV0alpha1DependenciesExtensions() *PluginMetaV0alpha1DependenciesExtensions { - return &PluginMetaV0alpha1DependenciesExtensions{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RouteTokenAuth struct { - Url *string `json:"url,omitempty"` - // +listType=set - Scopes []string `json:"scopes,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` -} - -// NewPluginMetaV0alpha1RouteTokenAuth creates a new PluginMetaV0alpha1RouteTokenAuth object. -func NewPluginMetaV0alpha1RouteTokenAuth() *PluginMetaV0alpha1RouteTokenAuth { - return &PluginMetaV0alpha1RouteTokenAuth{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RouteJwtTokenAuth struct { - Url *string `json:"url,omitempty"` - // +listType=set - Scopes []string `json:"scopes,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` -} - -// NewPluginMetaV0alpha1RouteJwtTokenAuth creates a new PluginMetaV0alpha1RouteJwtTokenAuth object. -func NewPluginMetaV0alpha1RouteJwtTokenAuth() *PluginMetaV0alpha1RouteJwtTokenAuth { - return &PluginMetaV0alpha1RouteJwtTokenAuth{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RouteUrlParams struct { - Name *string `json:"name,omitempty"` - Content *string `json:"content,omitempty"` -} - -// NewPluginMetaV0alpha1RouteUrlParams creates a new PluginMetaV0alpha1RouteUrlParams object. -func NewPluginMetaV0alpha1RouteUrlParams() *PluginMetaV0alpha1RouteUrlParams { - return &PluginMetaV0alpha1RouteUrlParams{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1IAMPermissions struct { - Action *string `json:"action,omitempty"` - Scope *string `json:"scope,omitempty"` -} - -// NewPluginMetaV0alpha1IAMPermissions creates a new PluginMetaV0alpha1IAMPermissions object. -func NewPluginMetaV0alpha1IAMPermissions() *PluginMetaV0alpha1IAMPermissions { - return &PluginMetaV0alpha1IAMPermissions{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RoleRolePermissions struct { - Action *string `json:"action,omitempty"` - Scope *string `json:"scope,omitempty"` -} - -// NewPluginMetaV0alpha1RoleRolePermissions creates a new PluginMetaV0alpha1RoleRolePermissions object. -func NewPluginMetaV0alpha1RoleRolePermissions() *PluginMetaV0alpha1RoleRolePermissions { - return &PluginMetaV0alpha1RoleRolePermissions{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RoleRole struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - // +listType=atomic - Permissions []PluginMetaV0alpha1RoleRolePermissions `json:"permissions,omitempty"` -} - -// NewPluginMetaV0alpha1RoleRole creates a new PluginMetaV0alpha1RoleRole object. -func NewPluginMetaV0alpha1RoleRole() *PluginMetaV0alpha1RoleRole { - return &PluginMetaV0alpha1RoleRole{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1ExtensionsAddedComponents struct { - // +listType=set - Targets []string `json:"targets"` - Title string `json:"title"` - Description *string `json:"description,omitempty"` -} - -// NewPluginMetaV0alpha1ExtensionsAddedComponents creates a new PluginMetaV0alpha1ExtensionsAddedComponents object. -func NewPluginMetaV0alpha1ExtensionsAddedComponents() *PluginMetaV0alpha1ExtensionsAddedComponents { - return &PluginMetaV0alpha1ExtensionsAddedComponents{ - Targets: []string{}, - } -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1ExtensionsAddedLinks struct { - // +listType=set - Targets []string `json:"targets"` - Title string `json:"title"` - Description *string `json:"description,omitempty"` -} - -// NewPluginMetaV0alpha1ExtensionsAddedLinks creates a new PluginMetaV0alpha1ExtensionsAddedLinks object. -func NewPluginMetaV0alpha1ExtensionsAddedLinks() *PluginMetaV0alpha1ExtensionsAddedLinks { - return &PluginMetaV0alpha1ExtensionsAddedLinks{ - Targets: []string{}, - } -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1ExtensionsExposedComponents struct { - Id string `json:"id"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` -} - -// NewPluginMetaV0alpha1ExtensionsExposedComponents creates a new PluginMetaV0alpha1ExtensionsExposedComponents object. -func NewPluginMetaV0alpha1ExtensionsExposedComponents() *PluginMetaV0alpha1ExtensionsExposedComponents { - return &PluginMetaV0alpha1ExtensionsExposedComponents{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1ExtensionsExtensionPoints struct { - Id string `json:"id"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` -} - -// NewPluginMetaV0alpha1ExtensionsExtensionPoints creates a new PluginMetaV0alpha1ExtensionsExtensionPoints object. -func NewPluginMetaV0alpha1ExtensionsExtensionPoints() *PluginMetaV0alpha1ExtensionsExtensionPoints { - return &PluginMetaV0alpha1ExtensionsExtensionPoints{} -} - -// +k8s:openapi-gen=true -type PluginMetaJSONDataType string - -const ( - PluginMetaJSONDataTypeApp PluginMetaJSONDataType = "app" - PluginMetaJSONDataTypeDatasource PluginMetaJSONDataType = "datasource" - PluginMetaJSONDataTypePanel PluginMetaJSONDataType = "panel" - PluginMetaJSONDataTypeRenderer PluginMetaJSONDataType = "renderer" -) - -// +k8s:openapi-gen=true -type PluginMetaJSONDataCategory string - -const ( - PluginMetaJSONDataCategoryTsdb PluginMetaJSONDataCategory = "tsdb" - PluginMetaJSONDataCategoryLogging PluginMetaJSONDataCategory = "logging" - PluginMetaJSONDataCategoryCloud PluginMetaJSONDataCategory = "cloud" - PluginMetaJSONDataCategoryTracing PluginMetaJSONDataCategory = "tracing" - PluginMetaJSONDataCategoryProfiling PluginMetaJSONDataCategory = "profiling" - PluginMetaJSONDataCategorySql PluginMetaJSONDataCategory = "sql" - PluginMetaJSONDataCategoryEnterprise PluginMetaJSONDataCategory = "enterprise" - PluginMetaJSONDataCategoryIot PluginMetaJSONDataCategory = "iot" - PluginMetaJSONDataCategoryOther PluginMetaJSONDataCategory = "other" -) - -// +k8s:openapi-gen=true -type PluginMetaJSONDataState string - -const ( - PluginMetaJSONDataStateAlpha PluginMetaJSONDataState = "alpha" - PluginMetaJSONDataStateBeta PluginMetaJSONDataState = "beta" -) - -// +k8s:openapi-gen=true -type PluginMetaIncludeType string - -const ( - PluginMetaIncludeTypeDashboard PluginMetaIncludeType = "dashboard" - PluginMetaIncludeTypePage PluginMetaIncludeType = "page" - PluginMetaIncludeTypePanel PluginMetaIncludeType = "panel" - PluginMetaIncludeTypeDatasource PluginMetaIncludeType = "datasource" -) - -// +k8s:openapi-gen=true -type PluginMetaIncludeRole string - -const ( - PluginMetaIncludeRoleAdmin PluginMetaIncludeRole = "Admin" - PluginMetaIncludeRoleEditor PluginMetaIncludeRole = "Editor" - PluginMetaIncludeRoleViewer PluginMetaIncludeRole = "Viewer" -) - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1DependenciesPluginsType string - -const ( - PluginMetaV0alpha1DependenciesPluginsTypeApp PluginMetaV0alpha1DependenciesPluginsType = "app" - PluginMetaV0alpha1DependenciesPluginsTypeDatasource PluginMetaV0alpha1DependenciesPluginsType = "datasource" - PluginMetaV0alpha1DependenciesPluginsTypePanel PluginMetaV0alpha1DependenciesPluginsType = "panel" -) diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index 84a55555c2b..428971aa0b5 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -20,12 +20,9 @@ import ( ) var ( - rawSchemaPluginMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"format":"date-time","type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boolean"},"metrics":{"type":"boolean"},"multiValueFilterOperators":{"type":"boolean"},"name":{"description":"Human-readable name of the plugin","type":"string"},"pascalName":{"type":"string"},"preload":{"type":"boolean"},"queryOptions":{"$ref":"#/components/schemas/QueryOptions"},"roles":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Role"},"type":"array"},"routes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Route"},"type":"array"},"skipDataQuery":{"type":"boolean"},"state":{"enum":["alpha","beta"],"type":"string"},"streaming":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"PluginMeta":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"QueryOptions":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"type":"object"},"Role":{"additionalProperties":false,"properties":{"grants":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"role":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"},"Route":{"additionalProperties":false,"properties":{"body":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"headers":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"jwtTokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"content":{"type":"string"},"name":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"pluginJSON":{"$ref":"#/components/schemas/JSONData"}},"required":["pluginJSON"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) - versionSchemaPluginMetav0alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaPluginMetav0alpha1, &versionSchemaPluginMetav0alpha1) - rawSchemaPluginInstallv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"PluginInstall":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external","cdn"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) - versionSchemaPluginInstallv0alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaPluginInstallv0alpha1, &versionSchemaPluginInstallv0alpha1) + rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"getMetaDependencies":{"type":"object","required":["grafanaDependency"],"properties":{"extensions":{"type":"object","properties":{"exposedComponents":{"description":"+listType=set","type":"array","items":{"type":"string"}}},"additionalProperties":false},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id","type","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["app","datasource","panel"]}},"additionalProperties":false}}},"additionalProperties":false},"getMetaEnterpriseFeatures":{"type":"object","properties":{"healthDiagnosticsErrors":{"description":"Allow additional properties","type":"boolean","default":false}},"additionalProperties":false},"getMetaExtensions":{"type":"object","properties":{"addedComponents":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"addedLinks":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaIAM":{"type":"object","properties":{"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaInclude":{"type":"object","properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"type":"string","enum":["Admin","Editor","Viewer"]},"type":{"type":"string","enum":["dashboard","page","panel","datasource"]},"uid":{"type":"string"}},"additionalProperties":false},"getMetaInfo":{"type":"object","required":["keywords","logos","updated","version"],"properties":{"author":{"description":"Optional fields","type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","type":"array","items":{"type":"string"}},"links":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false}},"logos":{"type":"object","required":["small","large"],"properties":{"large":{"type":"string"},"small":{"type":"string"}},"additionalProperties":false},"screenshots":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"}},"additionalProperties":false}},"updated":{"type":"string","format":"date-time"},"version":{"type":"string"}},"additionalProperties":false},"getMetaQueryOptions":{"type":"object","properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"additionalProperties":false},"getMetaRole":{"type":"object","properties":{"grants":{"description":"+listType=set","type":"array","items":{"type":"string"}},"role":{"type":"object","properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false}},"additionalProperties":false},"getMetaRoute":{"type":"object","properties":{"body":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"headers":{"description":"+listType=atomic","type":"array","items":{"type":"string"}},"jwtTokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"name":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external","cdn"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaPluginv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) ) var appManifestData = app.ManifestData{ @@ -35,22 +32,237 @@ var appManifestData = app.ManifestData{ Versions: []app.ManifestVersion{ { Name: "v0alpha1", - Served: false, + Served: true, Kinds: []app.ManifestVersionKind{ { - Kind: "PluginMeta", - Plural: "PluginMetas", + Kind: "Plugin", + Plural: "Plugins", Scope: "Namespaced", Conversion: false, - Schema: &versionSchemaPluginMetav0alpha1, - }, + Schema: &versionSchemaPluginv0alpha1, + Routes: map[string]spec3.PathProps{ + "/meta": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ - { - Kind: "PluginInstall", - Plural: "PluginInstalls", - Scope: "Namespaced", - Conversion: false, - Schema: &versionSchemaPluginInstallv0alpha1, + OperationId: "getMeta", + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "alerting": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Description: "Optional fields", + }, + }, + "annotations": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "autoEnabled": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "backend": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "buildMode": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "builtIn": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "category": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Enum: []interface{}{ + "tsdb", + "logging", + "cloud", + "tracing", + "profiling", + "sql", + "enterprise", + "iot", + "other", + }, + }, + }, + "dependencies": { + SchemaProps: spec.SchemaProps{ + + Description: "Dependency information", + Ref: spec.MustCreateRef("#/components/schemas/getMetaDependencies"), + }, + }, + "enterpriseFeatures": { + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getMetaEnterpriseFeatures"), + }, + }, + "executable": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "extensions": { + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getMetaExtensions"), + }, + }, + "hideFromList": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "iam": { + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getMetaIAM"), + }, + }, + "id": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Unique name of the plugin", + }, + }, + "includes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Description: "+listType=atomic", + }, + }, + "info": { + SchemaProps: spec.SchemaProps{ + + Description: "Metadata for the plugin", + Ref: spec.MustCreateRef("#/components/schemas/getMetaInfo"), + }, + }, + "logs": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "metrics": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "multiValueFilterOperators": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Human-readable name of the plugin", + }, + }, + "pascalName": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "preload": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "queryOptions": { + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getMetaQueryOptions"), + }, + }, + "roles": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Description: "+listType=atomic", + }, + }, + "routes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Description: "+listType=atomic", + }, + }, + "skipDataQuery": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Enum: []interface{}{ + "alpha", + "beta", + }, + }, + }, + "streaming": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "tracing": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Plugin type", + Enum: []interface{}{ + "app", + "datasource", + "panel", + "renderer", + }, + }, + }, + }, + Required: []string{ + "id", + "type", + "name", + "info", + "dependencies", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, }, }, Routes: app.ManifestVersionRoutes{ @@ -71,8 +283,7 @@ func RemoteManifest() app.Manifest { } var kindVersionToGoType = map[string]resource.Kind{ - "PluginMeta/v0alpha1": v0alpha1.PluginMetaKind(), - "PluginInstall/v0alpha1": v0alpha1.PluginInstallKind(), + "Plugin/v0alpha1": v0alpha1.PluginKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. @@ -82,7 +293,9 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist return goType, exists } -var customRouteToGoResponseType = map[string]any{} +var customRouteToGoResponseType = map[string]any{ + "v0alpha1|Plugin|meta|GET": v0alpha1.GetMeta{}, +} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. diff --git a/apps/plugins/pkg/app/app.go b/apps/plugins/pkg/app/app.go index 5503126704b..903e2580ab0 100644 --- a/apps/plugins/pkg/app/app.go +++ b/apps/plugins/pkg/app/app.go @@ -2,8 +2,11 @@ package app import ( "context" + "net/http" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" @@ -11,16 +14,15 @@ import ( "k8s.io/klog/v2" pluginsapi "github.com/grafana/grafana/apps/plugins/pkg/apis" + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" ) func New(cfg app.Config) (app.App, error) { - managedKinds := []simple.AppManagedKind{} - for _, kinds := range GetKinds() { - for _, k := range kinds { - managedKinds = append(managedKinds, simple.AppManagedKind{ - Kind: k, - }) - } + cfg.KubeConfig.APIPath = "apis" + clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()) + client, err := pluginsv0alpha1.NewPluginClientFromGenerator(clientGenerator) + if err != nil { + return nil, err } simpleConfig := simple.AppConfig{ @@ -33,7 +35,32 @@ func New(cfg app.Config) (app.App, error) { }, }, }, - ManagedKinds: managedKinds, + ManagedKinds: []simple.AppManagedKind{ + { + Kind: pluginsv0alpha1.PluginKind(), + CustomRoutes: simple.AppCustomRouteHandlers{ + simple.AppCustomRoute{ + Method: http.MethodGet, + Path: "meta", + }: func(ctx context.Context, w app.CustomRouteResponseWriter, req *app.CustomRouteRequest) error { + plugin, err := client.Get(ctx, resource.Identifier{ + Namespace: req.ResourceIdentifier.Namespace, + Name: req.ResourceIdentifier.Name, + }) + if err != nil { + return err + } + logging.DefaultLogger.Debug("fetched plugin", "plugin", plugin) + // TODO: Implement this in future PR + w.WriteHeader(http.StatusNotImplemented) + if _, err := w.Write([]byte("Not implemented")); err != nil { + return err + } + return nil + }, + }, + }, + }, } a, err := simple.NewApp(simpleConfig) diff --git a/apps/plugins/pkg/app/install/registrar.go b/apps/plugins/pkg/app/install/registrar.go index efb7de11080..fe747eb2806 100644 --- a/apps/plugins/pkg/app/install/registrar.go +++ b/apps/plugins/pkg/app/install/registrar.go @@ -41,12 +41,12 @@ type PluginInstall struct { Source Source } -func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alpha1.PluginInstall { +func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alpha1.Plugin { var url *string = nil if p.URL != "" { url = &p.URL } - return &pluginsv0alpha1.PluginInstall{ + return &pluginsv0alpha1.Plugin{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: p.ID, @@ -54,16 +54,16 @@ func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alph PluginInstallSourceAnnotation: p.Source, }, }, - Spec: pluginsv0alpha1.PluginInstallSpec{ + Spec: pluginsv0alpha1.PluginSpec{ Id: p.ID, Version: p.Version, Url: url, - Class: pluginsv0alpha1.PluginInstallSpecClass(p.Class), + Class: pluginsv0alpha1.PluginSpecClass(p.Class), }, } } -func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.PluginInstall) bool { +func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.Plugin) bool { update := p.ToPluginInstallV0Alpha1(existing.Namespace) if source, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && source != p.Source { return true @@ -92,7 +92,8 @@ func equalStringPointers(a, b *string) bool { type InstallRegistrar struct { clientGenerator resource.ClientGenerator - client *pluginsv0alpha1.PluginInstallClient + client *pluginsv0alpha1.PluginClient + clientErr error clientOnce sync.Once } @@ -103,24 +104,25 @@ func NewInstallRegistrar(clientGenerator resource.ClientGenerator) *InstallRegis } } -func (r *InstallRegistrar) GetClient() (*pluginsv0alpha1.PluginInstallClient, error) { +func (r *InstallRegistrar) GetClient() (*pluginsv0alpha1.PluginClient, error) { r.clientOnce.Do(func() { - client, err := pluginsv0alpha1.NewPluginInstallClientFromGenerator(r.clientGenerator) + client, err := pluginsv0alpha1.NewPluginClientFromGenerator(r.clientGenerator) if err != nil { + r.clientErr = err r.client = nil return } r.client = client }) - return r.client, nil + return r.client, r.clientErr } // Register creates or updates a plugin install in the registry. func (r *InstallRegistrar) Register(ctx context.Context, namespace string, install *PluginInstall) error { client, err := r.GetClient() if err != nil { - return nil + return err } identifier := resource.Identifier{ Namespace: namespace, @@ -132,9 +134,12 @@ func (r *InstallRegistrar) Register(ctx context.Context, namespace string, insta return err } - if existing != nil && install.ShouldUpdate(existing) { - _, err = client.Update(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.UpdateOptions{ResourceVersion: existing.ResourceVersion}) - return err + if existing != nil { + if install.ShouldUpdate(existing) { + _, err = client.Update(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.UpdateOptions{ResourceVersion: existing.ResourceVersion}) + return err + } + return nil } _, err = client.Create(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.CreateOptions{}) @@ -155,6 +160,10 @@ func (r *InstallRegistrar) Unregister(ctx context.Context, namespace string, nam if err != nil && !errorsK8s.IsNotFound(err) { return err } + // if the plugin doesn't exist, nothing to unregister + if existing == nil { + return nil + } // if the source is different, do not unregister if existingSource, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && existingSource != source { return nil diff --git a/apps/plugins/pkg/app/install/registrar_test.go b/apps/plugins/pkg/app/install/registrar_test.go new file mode 100644 index 00000000000..36359896ae6 --- /dev/null +++ b/apps/plugins/pkg/app/install/registrar_test.go @@ -0,0 +1,908 @@ +package install + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana-app-sdk/resource" + "github.com/stretchr/testify/require" + errorsK8s "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" +) + +func TestPluginInstall_ShouldUpdate(t *testing.T) { + baseExisting := &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + Annotations: map[string]string{ + PluginInstallSourceAnnotation: SourcePluginStore, + }, + }, + Spec: pluginsv0alpha1.PluginSpec{ + Id: "plugin-1", + Version: "1.0.0", + Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), + }, + } + + baseInstall := PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + } + + tests := []struct { + name string + modifyInstall func(*PluginInstall) + modifyExisting func(*pluginsv0alpha1.Plugin) + expectUpdate bool + }{ + { + name: "no changes", + expectUpdate: false, + }, + { + name: "version differs", + modifyInstall: func(pi *PluginInstall) { + pi.Version = "2.0.0" + }, + expectUpdate: true, + }, + { + name: "class differs", + modifyInstall: func(pi *PluginInstall) { + pi.Class = ClassCore + }, + expectUpdate: true, + }, + { + name: "url differs", + modifyInstall: func(pi *PluginInstall) { + pi.URL = "https://example.com/plugin.zip" + }, + expectUpdate: true, + }, + { + name: "source differs", + modifyExisting: func(existing *pluginsv0alpha1.Plugin) { + existing.Annotations[PluginInstallSourceAnnotation] = SourceUnknown + }, + expectUpdate: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + existing := baseExisting.DeepCopy() + install := baseInstall + + if tt.modifyExisting != nil { + tt.modifyExisting(existing) + } + if tt.modifyInstall != nil { + tt.modifyInstall(&install) + } + + require.Equal(t, tt.expectUpdate, install.ShouldUpdate(existing)) + }) + } +} + +func TestInstallRegistrar_Register(t *testing.T) { + tests := []struct { + name string + install *PluginInstall + existing *pluginsv0alpha1.Plugin + existingErr error + expectedCreates int + expectedUpdates int + expectError bool + }{ + { + name: "creates plugin when not found", + install: &PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + }, + existingErr: errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1"), + expectedCreates: 1, + }, + { + name: "updates plugin when fields change", + install: &PluginInstall{ + ID: "plugin-1", + Version: "2.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + }, + existing: &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + ResourceVersion: "7", + Annotations: map[string]string{ + PluginInstallSourceAnnotation: SourcePluginStore, + }, + }, + Spec: pluginsv0alpha1.PluginSpec{ + Id: "plugin-1", + Version: "1.0.0", + Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), + }, + }, + expectedUpdates: 1, + }, + { + name: "skips create when plugin matches", + install: &PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + }, + existing: &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + ResourceVersion: "9", + Annotations: map[string]string{ + PluginInstallSourceAnnotation: SourcePluginStore, + }, + }, + Spec: pluginsv0alpha1.PluginSpec{ + Id: "plugin-1", + Version: "1.0.0", + Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), + }, + }, + }, + { + name: "returns error on unexpected get failure", + install: &PluginInstall{ + ID: "plugin-err", + Version: "1.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + }, + existingErr: errorsK8s.NewInternalError(errors.New("boom")), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + createCalls := 0 + updateCalls := 0 + var receivedResourceVersions []string + var updatedPlugins []*pluginsv0alpha1.Plugin + + fakeClient := &fakePluginInstallClient{ + getFunc: func(context.Context, resource.Identifier) (*pluginsv0alpha1.Plugin, error) { + if tt.existingErr != nil { + return nil, tt.existingErr + } + if tt.existing == nil { + return nil, errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1") + } + return tt.existing.DeepCopy(), nil + }, + createFunc: func(context.Context, *pluginsv0alpha1.Plugin, resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) { + createCalls++ + return tt.install.ToPluginInstallV0Alpha1("org-1"), nil + }, + updateFunc: func(_ context.Context, obj *pluginsv0alpha1.Plugin, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) { + updateCalls++ + receivedResourceVersions = append(receivedResourceVersions, opts.ResourceVersion) + updatedPlugins = append(updatedPlugins, obj) + return obj, nil + }, + } + + registrar := NewInstallRegistrar(&fakeClientGenerator{client: fakeClient}) + + err := registrar.Register(ctx, "org-1", tt.install) + if tt.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.expectedCreates, createCalls) + require.Equal(t, tt.expectedUpdates, updateCalls) + + if tt.expectedUpdates > 0 { + require.Equal(t, []string{tt.existing.ResourceVersion}, receivedResourceVersions) + require.Len(t, updatedPlugins, 1) + require.Equal(t, tt.install.Version, updatedPlugins[0].Spec.Version) + } + }) + } +} + +func pluginGroupResource() schema.GroupResource { + return schema.GroupResource{Group: pluginsv0alpha1.APIGroup, Resource: "plugininstalls"} +} + +type fakePluginInstallClient struct { + listAllFunc func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) + getFunc func(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.Plugin, error) + createFunc func(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) + updateFunc func(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) + deleteFunc func(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error +} + +func (f *fakePluginInstallClient) Get(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.Plugin, error) { + if f.getFunc != nil { + return f.getFunc(ctx, identifier) + } + return nil, errorsK8s.NewNotFound(pluginGroupResource(), identifier.Name) +} + +func (f *fakePluginInstallClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) { + if f.listAllFunc != nil { + return f.listAllFunc(ctx, namespace, opts) + } + return &pluginsv0alpha1.PluginList{}, nil +} + +func (f *fakePluginInstallClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) { + return f.ListAll(ctx, namespace, opts) +} + +func (f *fakePluginInstallClient) Create(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) { + if f.createFunc != nil { + return f.createFunc(ctx, obj, opts) + } + return obj, nil +} + +func (f *fakePluginInstallClient) Update(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) { + if f.updateFunc != nil { + return f.updateFunc(ctx, obj, opts) + } + return obj, nil +} + +func (f *fakePluginInstallClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus pluginsv0alpha1.PluginStatus, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) { + return nil, nil +} + +func (f *fakePluginInstallClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*pluginsv0alpha1.Plugin, error) { + return nil, nil +} + +func (f *fakePluginInstallClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + if f.deleteFunc != nil { + return f.deleteFunc(ctx, identifier, opts) + } + return nil +} + +type fakeClientGenerator struct { + client *fakePluginInstallClient + shouldError bool +} + +func (f *fakeClientGenerator) ClientFor(resource.Kind) (resource.Client, error) { + if f.shouldError { + return nil, errors.New("client generation failed") + } + return &fakeResourceClient{client: f.client}, nil +} + +type fakeResourceClient struct { + client *fakePluginInstallClient +} + +func (f *fakeResourceClient) Get(ctx context.Context, identifier resource.Identifier) (resource.Object, error) { + return f.client.Get(ctx, identifier) +} + +func (f *fakeResourceClient) GetInto(ctx context.Context, identifier resource.Identifier, into resource.Object) error { + obj, err := f.client.Get(ctx, identifier) + if err != nil { + return err + } + if target, ok := into.(*pluginsv0alpha1.Plugin); ok { + *target = *obj + } + return nil +} + +func (f *fakeResourceClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return f.client.ListAll(ctx, namespace, options) +} + +func (f *fakeResourceClient) ListInto(ctx context.Context, namespace string, options resource.ListOptions, into resource.ListObject) error { + list, err := f.client.ListAll(ctx, namespace, options) + if err != nil { + return err + } + if target, ok := into.(*pluginsv0alpha1.PluginList); ok { + *target = *list + } + return nil +} + +func (f *fakeResourceClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) { + plugin := obj.(*pluginsv0alpha1.Plugin) + return f.client.Create(ctx, plugin, options) +} + +func (f *fakeResourceClient) CreateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions, into resource.Object) error { + created, err := f.Create(ctx, identifier, obj, options) + if err != nil { + return err + } + if plugin, ok := created.(*pluginsv0alpha1.Plugin); ok { + if target, ok := into.(*pluginsv0alpha1.Plugin); ok { + *target = *plugin + } + } + return nil +} + +func (f *fakeResourceClient) Update(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions) (resource.Object, error) { + plugin := obj.(*pluginsv0alpha1.Plugin) + return f.client.Update(ctx, plugin, options) +} + +func (f *fakeResourceClient) UpdateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions, into resource.Object) error { + updated, err := f.Update(ctx, identifier, obj, options) + if err != nil { + return err + } + if plugin, ok := updated.(*pluginsv0alpha1.Plugin); ok { + if target, ok := into.(*pluginsv0alpha1.Plugin); ok { + *target = *plugin + } + } + return nil +} + +func (f *fakeResourceClient) Patch(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions) (resource.Object, error) { + return nil, nil +} + +func (f *fakeResourceClient) PatchInto(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions, into resource.Object) error { + return nil +} + +func (f *fakeResourceClient) Delete(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error { + return f.client.Delete(ctx, identifier, options) +} + +func (f *fakeResourceClient) SubresourceRequest(ctx context.Context, identifier resource.Identifier, req resource.CustomRouteRequestOptions) ([]byte, error) { + return []byte{}, nil +} + +func (f *fakeResourceClient) Watch(ctx context.Context, namespace string, options resource.WatchOptions) (resource.WatchResponse, error) { + return &fakeWatchResponse{}, nil +} + +type fakeWatchResponse struct{} + +func (f *fakeWatchResponse) Stop() {} + +func (f *fakeWatchResponse) WatchEvents() <-chan resource.WatchEvent { + ch := make(chan resource.WatchEvent) + close(ch) + return ch +} + +func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { + tests := []struct { + name string + install PluginInstall + namespace string + validate func(*testing.T, *pluginsv0alpha1.Plugin) + }{ + { + name: "empty URL creates nil pointer", + install: PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + }, + namespace: "org-1", + validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { + require.Nil(t, p.Spec.Url) + }, + }, + { + name: "non-empty URL creates pointer", + install: PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + URL: "https://example.com/plugin.zip", + Class: ClassExternal, + Source: SourcePluginStore, + }, + namespace: "org-1", + validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { + require.NotNil(t, p.Spec.Url) + require.Equal(t, "https://example.com/plugin.zip", *p.Spec.Url) + }, + }, + { + name: "core class is mapped correctly", + install: PluginInstall{ + ID: "plugin-core", + Version: "2.0.0", + Class: ClassCore, + Source: SourcePluginStore, + }, + namespace: "org-2", + validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { + require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCore), p.Spec.Class) + }, + }, + { + name: "cdn class is mapped correctly", + install: PluginInstall{ + ID: "plugin-cdn", + Version: "3.0.0", + Class: ClassCDN, + Source: SourcePluginStore, + }, + namespace: "org-3", + validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { + require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCDN), p.Spec.Class) + }, + }, + { + name: "source annotation is set correctly", + install: PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + Class: ClassExternal, + Source: SourceUnknown, + }, + namespace: "org-1", + validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { + require.Equal(t, SourceUnknown, p.Annotations[PluginInstallSourceAnnotation]) + }, + }, + { + name: "namespace and name are set correctly", + install: PluginInstall{ + ID: "my-plugin", + Version: "1.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + }, + namespace: "my-namespace", + validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { + require.Equal(t, "my-namespace", p.Namespace) + require.Equal(t, "my-plugin", p.Name) + require.Equal(t, "my-plugin", p.Spec.Id) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.install.ToPluginInstallV0Alpha1(tt.namespace) + require.NotNil(t, result) + require.Equal(t, tt.namespace, result.Namespace) + require.Equal(t, tt.install.ID, result.Name) + require.Equal(t, tt.install.ID, result.Spec.Id) + require.Equal(t, tt.install.Version, result.Spec.Version) + tt.validate(t, result) + }) + } +} + +func TestEqualStringPointers(t *testing.T) { + str1 := "value1" + str2 := "value2" + str3 := "value1" + + tests := []struct { + name string + a *string + b *string + expected bool + }{ + { + name: "both nil", + a: nil, + b: nil, + expected: true, + }, + { + name: "first nil, second non-nil", + a: nil, + b: &str1, + expected: false, + }, + { + name: "first non-nil, second nil", + a: &str1, + b: nil, + expected: false, + }, + { + name: "both non-nil with same value", + a: &str1, + b: &str3, + expected: true, + }, + { + name: "both non-nil with different values", + a: &str1, + b: &str2, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := equalStringPointers(tt.a, tt.b) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { + existingURL := "https://old.example.com/plugin.zip" + newURL := "https://new.example.com/plugin.zip" + + tests := []struct { + name string + install PluginInstall + existingURL *string + expectUpdate bool + }{ + { + name: "URL transition from nil to non-nil", + install: PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + URL: newURL, + Class: ClassExternal, + Source: SourcePluginStore, + }, + existingURL: nil, + expectUpdate: true, + }, + { + name: "URL transition from non-nil to nil", + install: PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + URL: "", + Class: ClassExternal, + Source: SourcePluginStore, + }, + existingURL: &existingURL, + expectUpdate: true, + }, + { + name: "URL stays nil", + install: PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + URL: "", + Class: ClassExternal, + Source: SourcePluginStore, + }, + existingURL: nil, + expectUpdate: false, + }, + { + name: "URL stays same non-nil value", + install: PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + URL: existingURL, + Class: ClassExternal, + Source: SourcePluginStore, + }, + existingURL: &existingURL, + expectUpdate: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + existing := &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + Annotations: map[string]string{ + PluginInstallSourceAnnotation: SourcePluginStore, + }, + }, + Spec: pluginsv0alpha1.PluginSpec{ + Id: "plugin-1", + Version: "1.0.0", + Url: tt.existingURL, + Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), + }, + } + + require.Equal(t, tt.expectUpdate, tt.install.ShouldUpdate(existing)) + }) + } +} + +func TestInstallRegistrar_GetClient(t *testing.T) { + t.Run("successfully creates client on first call", func(t *testing.T) { + fakeClient := &fakePluginInstallClient{} + generator := &fakeClientGenerator{client: fakeClient} + registrar := NewInstallRegistrar(generator) + + client, err := registrar.GetClient() + require.NoError(t, err) + require.NotNil(t, client) + }) + + t.Run("returns same client on subsequent calls", func(t *testing.T) { + fakeClient := &fakePluginInstallClient{} + generator := &fakeClientGenerator{client: fakeClient} + registrar := NewInstallRegistrar(generator) + + client1, err1 := registrar.GetClient() + require.NoError(t, err1) + + client2, err2 := registrar.GetClient() + require.NoError(t, err2) + + require.Equal(t, client1, client2) + }) + + t.Run("returns error when client generation fails", func(t *testing.T) { + generator := &fakeClientGenerator{client: nil, shouldError: true} + registrar := NewInstallRegistrar(generator) + + client, err := registrar.GetClient() + require.Error(t, err) + require.Nil(t, client) + }) +} + +func TestInstallRegistrar_Register_ErrorCases(t *testing.T) { + tests := []struct { + name string + install *PluginInstall + setupClient func(*fakePluginInstallClient) + expectError bool + }{ + { + name: "create fails", + install: &PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + }, + setupClient: func(fc *fakePluginInstallClient) { + fc.getFunc = func(context.Context, resource.Identifier) (*pluginsv0alpha1.Plugin, error) { + return nil, errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1") + } + fc.createFunc = func(context.Context, *pluginsv0alpha1.Plugin, resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) { + return nil, errors.New("create failed") + } + }, + expectError: true, + }, + { + name: "update fails", + install: &PluginInstall{ + ID: "plugin-1", + Version: "2.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + }, + setupClient: func(fc *fakePluginInstallClient) { + fc.getFunc = func(context.Context, resource.Identifier) (*pluginsv0alpha1.Plugin, error) { + return &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + ResourceVersion: "5", + Annotations: map[string]string{ + PluginInstallSourceAnnotation: SourcePluginStore, + }, + }, + Spec: pluginsv0alpha1.PluginSpec{ + Id: "plugin-1", + Version: "1.0.0", + Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), + }, + }, nil + } + fc.updateFunc = func(context.Context, *pluginsv0alpha1.Plugin, resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) { + return nil, errors.New("update failed") + } + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + fakeClient := &fakePluginInstallClient{} + tt.setupClient(fakeClient) + + registrar := NewInstallRegistrar(&fakeClientGenerator{client: fakeClient}) + + err := registrar.Register(ctx, "org-1", tt.install) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestInstallRegistrar_Unregister(t *testing.T) { + tests := []struct { + name string + namespace string + pluginName string + source Source + existing *pluginsv0alpha1.Plugin + existingErr error + expectedCalls int + expectError bool + }{ + { + name: "successfully deletes plugin with matching source", + namespace: "org-1", + pluginName: "plugin-1", + source: SourcePluginStore, + existing: &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + Annotations: map[string]string{ + PluginInstallSourceAnnotation: SourcePluginStore, + }, + }, + }, + expectedCalls: 1, + }, + { + name: "plugin not found should not error", + namespace: "org-1", + pluginName: "plugin-nonexistent", + source: SourcePluginStore, + existingErr: errorsK8s.NewNotFound(pluginGroupResource(), "plugin-nonexistent"), + expectedCalls: 0, + expectError: false, + }, + { + name: "skips delete when source doesn't match", + namespace: "org-1", + pluginName: "plugin-1", + source: SourcePluginStore, + existing: &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + Annotations: map[string]string{ + PluginInstallSourceAnnotation: SourceUnknown, + }, + }, + }, + expectedCalls: 0, + }, + { + name: "returns error on unexpected get failure", + namespace: "org-1", + pluginName: "plugin-err", + source: SourcePluginStore, + existingErr: errorsK8s.NewInternalError(errors.New("get failed")), + expectedCalls: 0, + expectError: true, + }, + { + name: "delete failure returns error", + namespace: "org-1", + pluginName: "plugin-1", + source: SourcePluginStore, + existing: &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + Annotations: map[string]string{ + PluginInstallSourceAnnotation: SourcePluginStore, + }, + }, + }, + expectedCalls: 1, + expectError: true, + }, + { + name: "handles missing source annotation", + namespace: "org-1", + pluginName: "plugin-1", + source: SourcePluginStore, + existing: &pluginsv0alpha1.Plugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-1", + Name: "plugin-1", + Annotations: map[string]string{}, + }, + }, + expectedCalls: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + deleteCalls := 0 + + fakeClient := &fakePluginInstallClient{ + getFunc: func(context.Context, resource.Identifier) (*pluginsv0alpha1.Plugin, error) { + if tt.existingErr != nil { + return nil, tt.existingErr + } + if tt.existing == nil { + return nil, errorsK8s.NewNotFound(pluginGroupResource(), tt.pluginName) + } + return tt.existing.DeepCopy(), nil + }, + deleteFunc: func(context.Context, resource.Identifier, resource.DeleteOptions) error { + deleteCalls++ + if tt.name == "delete failure returns error" { + return errors.New("delete failed") + } + return nil + }, + } + + registrar := NewInstallRegistrar(&fakeClientGenerator{client: fakeClient}) + + err := registrar.Unregister(ctx, tt.namespace, tt.pluginName, tt.source) + + require.Equal(t, tt.expectedCalls, deleteCalls) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestInstallRegistrar_GetClientError(t *testing.T) { + t.Run("Register returns error with nil client", func(t *testing.T) { + ctx := context.Background() + generator := &fakeClientGenerator{client: nil, shouldError: true} + registrar := NewInstallRegistrar(generator) + + install := &PluginInstall{ + ID: "plugin-1", + Version: "1.0.0", + Class: ClassExternal, + Source: SourcePluginStore, + } + + err := registrar.Register(ctx, "org-1", install) + require.Error(t, err) + }) + + t.Run("Unregister returns error with nil client", func(t *testing.T) { + ctx := context.Background() + generator := &fakeClientGenerator{client: nil, shouldError: true} + registrar := NewInstallRegistrar(generator) + + err := registrar.Unregister(ctx, "org-1", "plugin-1", SourcePluginStore) + require.Error(t, err) + }) +} diff --git a/apps/plugins/pkg/app/storage.go b/apps/plugins/pkg/app/storage.go deleted file mode 100644 index 9fca277abfc..00000000000 --- a/apps/plugins/pkg/app/storage.go +++ /dev/null @@ -1,75 +0,0 @@ -package app - -import ( - "context" - "strings" - - 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" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/rest" - - claims "github.com/grafana/authlib/types" - pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" -) - -var ( - _ rest.Scoper = (*PluginMetaStorage)(nil) - _ rest.SingularNameProvider = (*PluginMetaStorage)(nil) - _ rest.Getter = (*PluginMetaStorage)(nil) - _ rest.Lister = (*PluginMetaStorage)(nil) - _ rest.Storage = (*PluginMetaStorage)(nil) - _ rest.TableConvertor = (*PluginMetaStorage)(nil) -) - -type PluginMetaStorage struct { - gr schema.GroupResource - namespacer claims.NamespaceFormatter - tableConverter rest.TableConvertor -} - -func NewPluginMetaStorage( - namespacer claims.NamespaceFormatter, -) *PluginMetaStorage { - gr := schema.GroupResource{ - Group: pluginsv0alpha1.PluginMetaKind().Group(), - Resource: strings.ToLower(pluginsv0alpha1.PluginMetaKind().Plural()), - } - return &PluginMetaStorage{ - gr: gr, - namespacer: namespacer, - tableConverter: rest.NewDefaultTableConvertor(gr), - } -} - -func (s *PluginMetaStorage) New() runtime.Object { - return pluginsv0alpha1.PluginMetaKind().ZeroValue() -} - -func (s *PluginMetaStorage) Destroy() {} - -func (s *PluginMetaStorage) NamespaceScoped() bool { - return true -} - -func (s *PluginMetaStorage) GetSingularName() string { - return strings.ToLower(pluginsv0alpha1.PluginMetaKind().Kind()) -} - -func (s *PluginMetaStorage) NewList() runtime.Object { - return pluginsv0alpha1.PluginMetaKind().ZeroListValue() -} - -func (s *PluginMetaStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return s.tableConverter.ConvertToTable(ctx, object, tableOptions) -} - -func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - return s.NewList(), nil -} - -func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return nil, apierrors.NewNotFound(s.gr, name) -} diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 3e5cac7119c..abd2a1ad561 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 + github.com/grafana/grafana-app-sdk v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.1 @@ -25,7 +25,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.46.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -50,12 +50,12 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 9ec1464aa8e..0af48867382 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -126,8 +126,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -138,14 +138,14 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 78aed88b2da..919602234e8 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -7,10 +7,10 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/google/uuid v1.6.0 github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 - github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 + github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b github.com/migueleliasweb/go-github-mock v1.1.0 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.32.0 @@ -44,7 +44,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.47.0 // indirect + github.com/grafana/grafana-app-sdk v0.48.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -68,13 +68,13 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 4f5d177e3d5..88f7288f7ee 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -62,16 +62,16 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z5Xpfp1WNYjUe23ginerWsHWUsRgOWrr3WGu3SlWs= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f/go.mod h1:RA8mP8KVIwKXBx3Ssqa/uEBABib5LvUWYPVMxrNvnP0= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= -github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 h1:cS0SlJGIlZbmDLctNj5vIYGemrJDLy25wwoiIyZWVN8= -github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b h1:rFjoqJFb2KxJ29K9ltuWRSsdA46SbN0GCxoQc36h5kg= +github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -156,8 +156,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -170,8 +170,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -198,8 +198,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -208,16 +208,16 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/provisioning/pkg/controller/historyjob.go b/apps/provisioning/pkg/controller/historyjob.go index 2e4e13f359d..bdfefb0b613 100644 --- a/apps/provisioning/pkg/controller/historyjob.go +++ b/apps/provisioning/pkg/controller/historyjob.go @@ -58,32 +58,42 @@ func NewHistoryJobController( func (c *HistoryJobController) cleanupJob(obj interface{}) { job, ok := obj.(*provisioning.HistoricJob) if !ok { - c.logger.Error("Expected HistoricJob but got", "type", obj) + c.logger.Error("unexpected object type - expected HistoricJob", "type", obj) return } age := time.Since(job.CreationTimestamp.Time) - if age > c.expirationTime { - namespace := job.Namespace - ctx, _, err := identity.WithProvisioningIdentity(context.Background(), namespace) - if err != nil { - c.logger.Error("Failed to set provisioning identity for cleanup", "error", err) + + // Only cleanup jobs older than expiration time + if age <= c.expirationTime { + return + } + + logger := c.logger.With( + "job", job.Name, + "namespace", job.Namespace, + "age", age, + ) + + logger.Debug("start cleanup expired historic job") + + namespace := job.Namespace + ctx, _, err := identity.WithProvisioningIdentity(context.Background(), namespace) + if err != nil { + logger.Error("failed to set provisioning identity", "error", err) + return + } + + ctx = request.WithNamespace(ctx, namespace) + err = c.client.HistoricJobs(job.Namespace).Delete(ctx, job.Name, metav1.DeleteOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + logger.Debug("historic job already deleted") return } - - ctx = request.WithNamespace(ctx, namespace) - err = c.client.HistoricJobs(job.Namespace).Delete(ctx, job.Name, metav1.DeleteOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - c.logger.Error("Failed to delete expired HistoryJob", - "namespace", job.Namespace, - "name", job.Name, - "age", age, - "error", err) - } else { - c.logger.Info("Deleted expired HistoryJob", - "namespace", job.Namespace, - "name", job.Name, - "age", age) - } + logger.Error("failed to delete expired historic job", "error", err) + return } + + logger.Info("deleted expired historic job") } diff --git a/apps/provisioning/pkg/jobs/validator.go b/apps/provisioning/pkg/jobs/validator.go new file mode 100644 index 00000000000..77232490b66 --- /dev/null +++ b/apps/provisioning/pkg/jobs/validator.go @@ -0,0 +1,172 @@ +package jobs + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/apps/provisioning/pkg/repository/git" + "github.com/grafana/grafana/apps/provisioning/pkg/safepath" +) + +// ValidateJob performs validation on the Job specification and returns an error if validation fails +func ValidateJob(job *provisioning.Job) error { + list := field.ErrorList{} + + // Validate action is specified + if job.Spec.Action == "" { + list = append(list, field.Required(field.NewPath("spec", "action"), "action must be specified")) + return toError(job.Name, list) // Early return since we can't validate further without knowing the action + } + + // Validate repository is specified + if job.Spec.Repository == "" { + list = append(list, field.Required(field.NewPath("spec", "repository"), "repository must be specified")) + } + + // Validate action-specific options + switch job.Spec.Action { + case provisioning.JobActionPull: + if job.Spec.Pull == nil { + list = append(list, field.Required(field.NewPath("spec", "pull"), "pull options required for pull action")) + } + // Pull options are simple, just incremental bool - no further validation needed + + case provisioning.JobActionPush: + if job.Spec.Push == nil { + list = append(list, field.Required(field.NewPath("spec", "push"), "push options required for push action")) + } else { + list = append(list, validateExportJobOptions(job.Spec.Push)...) + } + + case provisioning.JobActionPullRequest: + if job.Spec.PullRequest == nil { + list = append(list, field.Required(field.NewPath("spec", "pr"), "pull request options required for pr action")) + } + // PullRequest options are mostly informational - no strict validation needed + + case provisioning.JobActionMigrate: + if job.Spec.Migrate == nil { + list = append(list, field.Required(field.NewPath("spec", "migrate"), "migrate options required for migrate action")) + } + // Migrate options are simple - no further validation needed + + case provisioning.JobActionDelete: + if job.Spec.Delete == nil { + list = append(list, field.Required(field.NewPath("spec", "delete"), "delete options required for delete action")) + } else { + list = append(list, validateDeleteJobOptions(job.Spec.Delete)...) + } + + case provisioning.JobActionMove: + if job.Spec.Move == nil { + list = append(list, field.Required(field.NewPath("spec", "move"), "move options required for move action")) + } else { + list = append(list, validateMoveJobOptions(job.Spec.Move)...) + } + default: + list = append(list, field.Invalid(field.NewPath("spec", "action"), job.Spec.Action, "invalid action")) + } + + return toError(job.Name, list) +} + +// toError converts a field.ErrorList to an error, returning nil if the list is empty +func toError(name string, list field.ErrorList) error { + if len(list) == 0 { + return nil + } + return apierrors.NewInvalid( + provisioning.JobResourceInfo.GroupVersionKind().GroupKind(), + name, list) +} + +// validateExportJobOptions validates export (push) job options +func validateExportJobOptions(opts *provisioning.ExportJobOptions) field.ErrorList { + list := field.ErrorList{} + + // Validate branch name if specified + if opts.Branch != "" { + if !git.IsValidGitBranchName(opts.Branch) { + list = append(list, field.Invalid(field.NewPath("spec", "push", "branch"), opts.Branch, "invalid git branch name")) + } + } + + // Validate path if specified + if opts.Path != "" { + if err := safepath.IsSafe(opts.Path); err != nil { + list = append(list, field.Invalid(field.NewPath("spec", "push", "path"), opts.Path, err.Error())) + } + } + + return list +} + +// validateDeleteJobOptions validates delete job options +func validateDeleteJobOptions(opts *provisioning.DeleteJobOptions) field.ErrorList { + list := field.ErrorList{} + + // At least one of paths or resources must be specified + if len(opts.Paths) == 0 && len(opts.Resources) == 0 { + list = append(list, field.Required(field.NewPath("spec", "delete"), "at least one path or resource must be specified")) + return list + } + + // Validate paths + for i, p := range opts.Paths { + if err := safepath.IsSafe(p); err != nil { + list = append(list, field.Invalid(field.NewPath("spec", "delete", "paths").Index(i), p, err.Error())) + } + } + + // Validate resources + for i, r := range opts.Resources { + if r.Name == "" { + list = append(list, field.Required(field.NewPath("spec", "delete", "resources").Index(i).Child("name"), "resource name is required")) + } + if r.Kind == "" { + list = append(list, field.Required(field.NewPath("spec", "delete", "resources").Index(i).Child("kind"), "resource kind is required")) + } + } + + return list +} + +// validateMoveJobOptions validates move job options +func validateMoveJobOptions(opts *provisioning.MoveJobOptions) field.ErrorList { + list := field.ErrorList{} + + // At least one of paths or resources must be specified + if len(opts.Paths) == 0 && len(opts.Resources) == 0 { + list = append(list, field.Required(field.NewPath("spec", "move"), "at least one path or resource must be specified")) + return list + } + + // Target path is required + if opts.TargetPath == "" { + list = append(list, field.Required(field.NewPath("spec", "move", "targetPath"), "target path is required")) + } else { + if err := safepath.IsSafe(opts.TargetPath); err != nil { + list = append(list, field.Invalid(field.NewPath("spec", "move", "targetPath"), opts.TargetPath, err.Error())) + } + } + + // Validate source paths + for i, p := range opts.Paths { + if err := safepath.IsSafe(p); err != nil { + list = append(list, field.Invalid(field.NewPath("spec", "move", "paths").Index(i), p, err.Error())) + } + } + + // Validate resources + for i, r := range opts.Resources { + if r.Name == "" { + list = append(list, field.Required(field.NewPath("spec", "move", "resources").Index(i).Child("name"), "resource name is required")) + } + if r.Kind == "" { + list = append(list, field.Required(field.NewPath("spec", "move", "resources").Index(i).Child("kind"), "resource kind is required")) + } + } + + return list +} diff --git a/apps/provisioning/pkg/jobs/validator_test.go b/apps/provisioning/pkg/jobs/validator_test.go new file mode 100644 index 00000000000..fdd29598ebc --- /dev/null +++ b/apps/provisioning/pkg/jobs/validator_test.go @@ -0,0 +1,593 @@ +package jobs + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" +) + +func TestValidateJob(t *testing.T) { + tests := []struct { + name string + job *provisioning.Job + wantErr bool + validateError func(t *testing.T, err error) + }{ + { + name: "valid pull job", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPull, + Repository: "test-repo", + Pull: &provisioning.SyncJobOptions{ + Incremental: true, + }, + }, + }, + wantErr: false, + }, + { + name: "missing action", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Repository: "test-repo", + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.action: Required value") + }, + }, + { + name: "invalid action", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobAction("invalid"), + Repository: "test-repo", + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.action: Invalid value") + }, + }, + { + name: "missing repository", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPull, + Pull: &provisioning.SyncJobOptions{ + Incremental: true, + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.repository: Required value") + }, + }, + { + name: "pull action without pull options", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPull, + Repository: "test-repo", + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.pull: Required value") + }, + }, + { + name: "push action without push options", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPush, + Repository: "test-repo", + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.push: Required value") + }, + }, + { + name: "valid push job with valid branch", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPush, + Repository: "test-repo", + Push: &provisioning.ExportJobOptions{ + Branch: "main", + Message: "Test commit", + }, + }, + }, + wantErr: false, + }, + { + name: "push job with invalid branch name", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPush, + Repository: "test-repo", + Push: &provisioning.ExportJobOptions{ + Branch: "feature..branch", // Invalid: contains consecutive dots + Message: "Test commit", + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.push.branch") + require.Contains(t, err.Error(), "invalid git branch name") + }, + }, + { + name: "push job with invalid path", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPush, + Repository: "test-repo", + Push: &provisioning.ExportJobOptions{ + Path: "../../../etc/passwd", // Invalid: path traversal + Message: "Test commit", + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.push.path") + }, + }, + { + name: "delete action without options", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Repository: "test-repo", + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.delete: Required value") + }, + }, + { + name: "delete action without paths or resources", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Repository: "test-repo", + Delete: &provisioning.DeleteJobOptions{}, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "at least one path or resource must be specified") + }, + }, + { + name: "valid delete action with paths", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Repository: "test-repo", + Delete: &provisioning.DeleteJobOptions{ + Paths: []string{"dashboard.json", "folder/other.json"}, + }, + }, + }, + wantErr: false, + }, + { + name: "valid delete action with resources", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Repository: "test-repo", + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "my-dashboard", + Kind: "Dashboard", + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "delete action with invalid path", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Repository: "test-repo", + Delete: &provisioning.DeleteJobOptions{ + Paths: []string{"../../etc/passwd"}, // Invalid: path traversal + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.delete.paths[0]") + }, + }, + { + name: "delete action with resource missing name", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Repository: "test-repo", + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Kind: "Dashboard", + }, + }, + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.delete.resources[0].name") + }, + }, + { + name: "move action without options", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.move: Required value") + }, + }, + { + name: "move action without paths or resources", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + Move: &provisioning.MoveJobOptions{ + TargetPath: "new-location/", + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "at least one path or resource must be specified") + }, + }, + { + name: "move action without target path", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + Move: &provisioning.MoveJobOptions{ + Paths: []string{"dashboard.json"}, + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.move.targetPath: Required value") + }, + }, + { + name: "valid move action", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + Move: &provisioning.MoveJobOptions{ + Paths: []string{"old-location/dashboard.json"}, + TargetPath: "new-location/", + }, + }, + }, + wantErr: false, + }, + { + name: "move action with invalid target path", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + Move: &provisioning.MoveJobOptions{ + Paths: []string{"dashboard.json"}, + TargetPath: "../../../etc/", // Invalid: path traversal + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.move.targetPath") + }, + }, + { + name: "valid migrate job", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMigrate, + Repository: "test-repo", + Migrate: &provisioning.MigrateJobOptions{ + History: true, + Message: "Migrate from legacy", + }, + }, + }, + wantErr: false, + }, + { + name: "migrate action without migrate options", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMigrate, + Repository: "test-repo", + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.migrate: Required value") + }, + }, + { + name: "valid pr job", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPullRequest, + Repository: "test-repo", + PullRequest: &provisioning.PullRequestJobOptions{ + PR: 123, + Ref: "refs/pull/123/head", + }, + }, + }, + wantErr: false, + }, + { + name: "delete action with resource missing kind", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Repository: "test-repo", + Delete: &provisioning.DeleteJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "my-dashboard", + }, + }, + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.delete.resources[0].kind") + }, + }, + { + name: "move action with valid resources", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + Move: &provisioning.MoveJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "my-dashboard", + Kind: "Dashboard", + }, + }, + TargetPath: "new-location/", + }, + }, + }, + wantErr: false, + }, + { + name: "move action with resource missing kind", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + Move: &provisioning.MoveJobOptions{ + Resources: []provisioning.ResourceRef{ + { + Name: "my-dashboard", + }, + }, + TargetPath: "new-location/", + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.move.resources[0].kind") + }, + }, + { + name: "move action with both paths and resources", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + Move: &provisioning.MoveJobOptions{ + Paths: []string{"dashboard.json"}, + Resources: []provisioning.ResourceRef{ + { + Name: "my-dashboard", + Kind: "Dashboard", + }, + }, + TargetPath: "new-location/", + }, + }, + }, + wantErr: false, + }, + { + name: "move action with invalid source path", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionMove, + Repository: "test-repo", + Move: &provisioning.MoveJobOptions{ + Paths: []string{"../invalid/path"}, + TargetPath: "valid/target/", + }, + }, + }, + wantErr: true, + validateError: func(t *testing.T, err error) { + require.Contains(t, err.Error(), "spec.move.paths[0]") + }, + }, + { + name: "delete action with both paths and resources", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionDelete, + Repository: "test-repo", + Delete: &provisioning.DeleteJobOptions{ + Paths: []string{"dashboard.json"}, + Resources: []provisioning.ResourceRef{ + { + Name: "my-dashboard", + Kind: "Dashboard", + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "push action with valid path", + job: &provisioning.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + }, + Spec: provisioning.JobSpec{ + Action: provisioning.JobActionPush, + Repository: "test-repo", + Push: &provisioning.ExportJobOptions{ + Path: "some/valid/path", + Message: "Test commit", + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateJob(tt.job) + if tt.wantErr { + require.Error(t, err) + if tt.validateError != nil { + tt.validateError(t, err) + } + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/apps/scope/go.mod b/apps/scope/go.mod index e769828a09d..c2efa6601d8 100644 --- a/apps/scope/go.mod +++ b/apps/scope/go.mod @@ -30,7 +30,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/text v0.30.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/scope/go.sum b/apps/scope/go.sum index 80edbe531d4..f720929510d 100644 --- a/apps/scope/go.sum +++ b/apps/scope/go.sum @@ -73,8 +73,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/apps/scope/pkg/apis/scope/v0alpha1/types.go b/apps/scope/pkg/apis/scope/v0alpha1/types.go index 799195b2832..b0f58fdfd44 100644 --- a/apps/scope/pkg/apis/scope/v0alpha1/types.go +++ b/apps/scope/pkg/apis/scope/v0alpha1/types.go @@ -146,7 +146,12 @@ type ScopeNodeSpec struct { NodeType NodeType `json:"nodeType"` // container | leaf - Title string `json:"title"` + Title string `json:"title"` + //+optional + // Displays next to the title to provide more context. + SubTitle string `json:"subTitle,omitempty"` + + //+optional Description string `json:"description,omitempty"` DisableMultiSelect bool `json:"disableMultiSelect"` diff --git a/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go b/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go index 243b0ff2c2e..a0b0e90b557 100644 --- a/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go +++ b/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go @@ -838,6 +838,13 @@ func schema_pkg_apis_scope_v0alpha1_ScopeNodeSpec(ref common.ReferenceCallback) Format: "", }, }, + "subTitle": { + SchemaProps: spec.SchemaProps{ + Description: "Displays next to the title to provide more context.", + Type: []string{"string"}, + Format: "", + }, + }, "description": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, diff --git a/apps/sdk.mk b/apps/sdk.mk index 04e8f4ade70..01d88064558 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.47.0 +APP_SDK_VERSION = v0.48.1 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index 4d9d57f91b2..35e7485e080 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/secret go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 + github.com/grafana/grafana-app-sdk v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 google.golang.org/grpc v1.76.0 @@ -29,7 +29,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.46.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -55,12 +55,12 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/client-go v0.34.1 // indirect diff --git a/apps/secret/go.sum b/apps/secret/go.sum index a4efed39b82..fcbebea1537 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -37,10 +37,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -138,8 +138,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -150,14 +150,14 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 19d4fdc9306..e17faafae16 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/shorturl go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.47.0 - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.1 k8s.io/klog/v2 v2.130.1 @@ -73,14 +73,14 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index 77009d338af..9fda293a4b4 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -56,10 +56,10 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba h1:Qam8QzVRsyZN39zgZ9Vj6e8PEfswvv2McnqCZ/v5NcI= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba/go.mod h1:rlJ/mmE0RQolOB2+HV3+bw+ZifHyPDQurBwZEus+Wm0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -169,16 +169,16 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -191,20 +191,20 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/apps/shorturl/kinds/manifest.cue b/apps/shorturl/kinds/manifest.cue index 27519d040af..c078f591c81 100644 --- a/apps/shorturl/kinds/manifest.cue +++ b/apps/shorturl/kinds/manifest.cue @@ -1,68 +1,11 @@ package kinds manifest: { - // appName is the unique name of your app. It is used to reference the app from other config objects, - // and to generate the group used by your app in the app platform API. appName: "shorturl" groupOverride: "shorturl.grafana.app" - // groupOverride can be used to specify a non-appName-based API group. - // By default, an app's API group is LOWER(REPLACE(appName, '-', '')).ext.grafana.com, - // but there are cases where this needs to be changed. - // Keep in mind that changing this after an app is deployed can cause problems with clients and/or kind data. - // groupOverride: foo.ext.grafana.app - - // versions is a map of versions supported by your app. Version names should follow the format "v" or - // "v(alpha|beta)". Each version contains the kinds your app manages for that version. - // If your app needs access to kinds managed by another app, use permissions.accessKinds to allow your app access. versions: { - "v1alpha1": v1alpha1 - } - // extraPermissions contains any additional permissions your app may require to function. - // Your app will always have all permissions for each kind it manages (the items defined in 'kinds'). - extraPermissions: { - // If your app needs access to additional kinds supplied by other apps, you can list them here - accessKinds: [ - // Here is an example for your app accessing the playlist kind for reads and watch - // { - // group: "playlist.grafana.app" - // resource: "playlists" - // actions: ["get","list","watch"] - // } - ] + "v1beta1": { + kinds: [shorturl] + } } } - -// v1alpha1 is the v1alpha1 version of the app's API. -// It includes kinds which the v1alpha1 API serves, and (future) custom routes served globally from the v1alpha1 version. -v1alpha1: { - // kinds is the list of kinds served by this version - kinds: [shorturl] - // [OPTIONAL] - // served indicates whether this particular version is served by the API server. - // served should be set to false before a version is removed from the manifest entirely. - // served defaults to true if not present. - served: true - // [OPTIONAL] - // Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind. - // If not present, default values within the codegen trait are used. - // If you wish to specify codegen per-version, put this section in the version's object - // (for example, v1alpha1) instead. - codegen: { - // [OPTIONAL] - // ts contains TypeScript code generation properties for the kind - ts: { - // [OPTIONAL] - // enabled indicates whether the CLI should generate front-end TypeScript code for the kind. - // Defaults to true if not present. - enabled: true - } - // [OPTIONAL] - // go contains go code generation properties for the kind - go: { - // [OPTIONAL] - // enabled indicates whether the CLI should generate back-end go code for the kind. - // Defaults to true if not present. - enabled: true - } - } -} diff --git a/apps/shorturl/pkg/apis/shorturl/v1beta1/constants.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/constants.go new file mode 100644 index 00000000000..2fe732d2371 --- /dev/null +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/constants.go @@ -0,0 +1,18 @@ +package v1beta1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "shorturl.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1beta1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_client_gen.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_client_gen.go similarity index 99% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_client_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_client_gen.go index 1c2a81b3c34..6528fd89b98 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_client_gen.go +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_client_gen.go @@ -1,4 +1,4 @@ -package v1alpha1 +package v1beta1 import ( "context" diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_codec_gen.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_codec_gen.go similarity index 97% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_codec_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_codec_gen.go index 15d7493bb80..37b343f5a53 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_codec_gen.go +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_codec_gen.go @@ -2,7 +2,7 @@ // Code generated by grafana-app-sdk. DO NOT EDIT. // -package v1alpha1 +package v1beta1 import ( "encoding/json" diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_response_types_gen.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_getgoto_response_types_gen.go similarity index 92% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_response_types_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_getgoto_response_types_gen.go index dfe47f8cbc1..c622afb824c 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_getgoto_response_types_gen.go +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_getgoto_response_types_gen.go @@ -1,6 +1,6 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. -package v1alpha1 +package v1beta1 // +k8s:openapi-gen=true type GetGoto struct { diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_metadata_gen.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_metadata_gen.go similarity index 98% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_metadata_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_metadata_gen.go index e7509c1a38e..bb2aa3dda44 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_metadata_gen.go +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_metadata_gen.go @@ -1,6 +1,6 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. -package v1alpha1 +package v1beta1 import ( time "time" diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_object_gen.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_object_gen.go similarity index 99% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_object_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_object_gen.go index 734af74aae3..cfa817bc239 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_object_gen.go +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_object_gen.go @@ -2,7 +2,7 @@ // Code generated by grafana-app-sdk. DO NOT EDIT. // -package v1alpha1 +package v1beta1 import ( "fmt" diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_schema_gen.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_schema_gen.go similarity index 89% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_schema_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_schema_gen.go index 78f0d5839d2..2cebbc15437 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_schema_gen.go +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_schema_gen.go @@ -2,7 +2,7 @@ // Code generated by grafana-app-sdk. DO NOT EDIT. // -package v1alpha1 +package v1beta1 import ( "github.com/grafana/grafana-app-sdk/resource" @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaShortURL = resource.NewSimpleSchema("shorturl.grafana.app", "v1alpha1", &ShortURL{}, &ShortURLList{}, resource.WithKind("ShortURL"), + schemaShortURL = resource.NewSimpleSchema("shorturl.grafana.app", "v1beta1", &ShortURL{}, &ShortURLList{}, resource.WithKind("ShortURL"), resource.WithPlural("shorturls"), resource.WithScope(resource.NamespacedScope)) kindShortURL = resource.Kind{ Schema: schemaShortURL, diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_spec_gen.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_spec_gen.go similarity index 95% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_spec_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_spec_gen.go index 56a453e5b69..92b14036f7a 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_spec_gen.go +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_spec_gen.go @@ -1,6 +1,6 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. -package v1alpha1 +package v1beta1 // +k8s:openapi-gen=true type ShortURLSpec struct { diff --git a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_status_gen.go b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_status_gen.go similarity index 99% rename from apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_status_gen.go rename to apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_status_gen.go index cfbeb15b338..d8400bd7882 100644 --- a/apps/shorturl/pkg/apis/shorturl/v1alpha1/shorturl_status_gen.go +++ b/apps/shorturl/pkg/apis/shorturl/v1beta1/shorturl_status_gen.go @@ -1,6 +1,6 @@ // Code generated - EDITING IS FUTILE. DO NOT EDIT. -package v1alpha1 +package v1beta1 // +k8s:openapi-gen=true type ShortURLstatusOperatorState struct { diff --git a/apps/shorturl/pkg/apis/shorturl_manifest.go b/apps/shorturl/pkg/apis/shorturl_manifest.go index 8cc2e9acf90..4e8acbe5797 100644 --- a/apps/shorturl/pkg/apis/shorturl_manifest.go +++ b/apps/shorturl/pkg/apis/shorturl_manifest.go @@ -16,22 +16,22 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" - v1alpha1 "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" + v1beta1 "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" ) var ( - rawSchemaShortURLv1alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"ShortURL":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"path":{"description":"The original path to where the short url is linking too e.g. https://localhost:3000/eer8i1kictngga/new-dashboard-with-lib-panel","type":"string"}},"required":["path"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"lastSeenAt":{"description":"The last time the short URL was used, 0 is the initial value","type":"integer"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"required":["lastSeenAt"],"type":"object"}}`) - versionSchemaShortURLv1alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaShortURLv1alpha1, &versionSchemaShortURLv1alpha1) + rawSchemaShortURLv1beta1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"ShortURL":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"path":{"description":"The original path to where the short url is linking too e.g. https://localhost:3000/eer8i1kictngga/new-dashboard-with-lib-panel","type":"string"}},"required":["path"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"lastSeenAt":{"description":"The last time the short URL was used, 0 is the initial value","type":"integer"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"required":["lastSeenAt"],"type":"object"}}`) + versionSchemaShortURLv1beta1 app.VersionSchema + _ = json.Unmarshal(rawSchemaShortURLv1beta1, &versionSchemaShortURLv1beta1) ) var appManifestData = app.ManifestData{ AppName: "shorturl", Group: "shorturl.grafana.app", - PreferredVersion: "v1alpha1", + PreferredVersion: "v1beta1", Versions: []app.ManifestVersion{ { - Name: "v1alpha1", + Name: "v1beta1", Served: true, Kinds: []app.ManifestVersionKind{ { @@ -47,7 +47,7 @@ var appManifestData = app.ManifestData{ }, }, }, - Schema: &versionSchemaShortURLv1alpha1, + Schema: &versionSchemaShortURLv1beta1, Routes: map[string]spec3.PathProps{ "/goto": { Get: &spec3.Operation{ @@ -106,7 +106,7 @@ func RemoteManifest() app.Manifest { } var kindVersionToGoType = map[string]resource.Kind{ - "ShortURL/v1alpha1": v1alpha1.ShortURLKind(), + "ShortURL/v1beta1": v1beta1.ShortURLKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. @@ -117,7 +117,7 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist } var customRouteToGoResponseType = map[string]any{ - "v1alpha1|ShortURL|goto|GET": v1alpha1.GetGoto{}, + "v1beta1|ShortURL|goto|GET": v1beta1.GetGoto{}, } // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. diff --git a/apps/shorturl/pkg/app/app.go b/apps/shorturl/pkg/app/app.go index 4c85ef97435..dc2e604aba3 100644 --- a/apps/shorturl/pkg/app/app.go +++ b/apps/shorturl/pkg/app/app.go @@ -18,7 +18,7 @@ import ( "github.com/grafana/grafana-app-sdk/operator" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" - shorturlv1alpha1 "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" + shorturlv1beta1 "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/identity" ) @@ -30,11 +30,12 @@ var ( func New(cfg app.Config) (app.App, error) { cfg.KubeConfig.APIPath = "apis" - client, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()). - ClientFor(shorturlv1alpha1.ShortURLKind()) + tmp, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()). + ClientFor(shorturlv1beta1.ShortURLKind()) if err != nil { return nil, fmt.Errorf("unable to create client") } + client := shorturlv1beta1.NewShortURLClient(tmp) simpleConfig := simple.AppConfig{ Name: "shorturl", @@ -48,11 +49,11 @@ func New(cfg app.Config) (app.App, error) { }, ManagedKinds: []simple.AppManagedKind{ { - Kind: shorturlv1alpha1.ShortURLKind(), + Kind: shorturlv1beta1.ShortURLKind(), Validator: &simple.Validator{ ValidateFunc: func(ctx context.Context, req *app.AdmissionRequest) error { // Cast the incoming object to ShortURL for validation - shortURL, ok := req.Object.(*shorturlv1alpha1.ShortURL) + shortURL, ok := req.Object.(*shorturlv1beta1.ShortURL) if !ok { return fmt.Errorf("expected ShortURL object, got %T", req.Object) } @@ -81,8 +82,8 @@ func New(cfg app.Config) (app.App, error) { Name: req.ResourceIdentifier.Name, } - info := &shorturlv1alpha1.ShortURL{} - if err := client.GetInto(ctx, id, info); err != nil { + info, err := client.Get(ctx, id) + if err != nil { return err } @@ -93,13 +94,18 @@ func New(cfg app.Config) (app.App, error) { if err != nil { logging.FromContext(ctx).Warn("unable to create background identity", "err", err) } else { - _, _ = client.Update(ctx, id, info, resource.UpdateOptions{}) + _, err = client.UpdateStatus(ctx, id, info.Status, resource.UpdateOptions{ + ResourceVersion: info.ResourceVersion, + }) + if err != nil { + logging.FromContext(ctx).Warn("unable to update status", "err", err) + } } }() url = url + "/" + info.Spec.Path if req.URL.Query().Get("redirect") == "false" { // helpful for testing - return json.NewEncoder(w).Encode(shorturlv1alpha1.GetGoto{ + return json.NewEncoder(w).Encode(shorturlv1beta1.GetGoto{ Url: url, }) } @@ -127,10 +133,10 @@ func New(cfg app.Config) (app.App, error) { func GetKinds() map[schema.GroupVersion][]resource.Kind { gv := schema.GroupVersion{ - Group: shorturlv1alpha1.ShortURLKind().Group(), - Version: shorturlv1alpha1.ShortURLKind().Version(), + Group: shorturlv1beta1.ShortURLKind().Group(), + Version: shorturlv1beta1.ShortURLKind().Version(), } return map[schema.GroupVersion][]resource.Kind{ - gv: {shorturlv1alpha1.ShortURLKind()}, + gv: {shorturlv1beta1.ShortURLKind()}, } } diff --git a/apps/shorturl/plugin/src/generated/shorturl/v1alpha1/shorturl_object_gen.ts b/apps/shorturl/plugin/src/generated/shorturl/v1beta1/shorturl_object_gen.ts similarity index 100% rename from apps/shorturl/plugin/src/generated/shorturl/v1alpha1/shorturl_object_gen.ts rename to apps/shorturl/plugin/src/generated/shorturl/v1beta1/shorturl_object_gen.ts diff --git a/apps/shorturl/plugin/src/generated/shorturl/v1beta1/types.metadata.gen.ts b/apps/shorturl/plugin/src/generated/shorturl/v1beta1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/shorturl/plugin/src/generated/shorturl/v1beta1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/shorturl/plugin/src/generated/shorturl/v1alpha1/types.spec.gen.ts b/apps/shorturl/plugin/src/generated/shorturl/v1beta1/types.spec.gen.ts similarity index 100% rename from apps/shorturl/plugin/src/generated/shorturl/v1alpha1/types.spec.gen.ts rename to apps/shorturl/plugin/src/generated/shorturl/v1beta1/types.spec.gen.ts diff --git a/apps/shorturl/plugin/src/generated/shorturl/v1alpha1/types.status.gen.ts b/apps/shorturl/plugin/src/generated/shorturl/v1beta1/types.status.gen.ts similarity index 100% rename from apps/shorturl/plugin/src/generated/shorturl/v1alpha1/types.status.gen.ts rename to apps/shorturl/plugin/src/generated/shorturl/v1beta1/types.status.gen.ts diff --git a/conf/defaults.ini b/conf/defaults.ini index 977d0fa8783..e25fb0c1715 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -340,6 +340,10 @@ application_insights_connection_string = # Optional. Specifies an Application Insights endpoint URL where the endpoint string is wrapped in backticks ``. application_insights_endpoint_url = +# Optional, defaults to true. Configure automatic route tracking for single page applications in Application Insights. +# See https://learn.microsoft.com/en-us/azure/azure-monitor/app/application-insights-faq#is-there-a-way-to-see-fewer-events-per-transaction-when-i-use-the-application-insights-javascript-sdk +application_insights_auto_route_tracking = true + # Controls if the UI contains any links to user feedback forms feedback_links_enabled = true @@ -443,7 +447,6 @@ content_security_policy_report_only_template = """script-src 'self' 'unsafe-eval csrf_always_check = false # Comma-separated list of plugins ids that will be loaded inside the frontend sandbox -# Currently behind the feature flag pluginsFrontendSandbox enable_frontend_sandbox_for_plugins = # Comma-separated list of paths for POST/PUT URL in actions. Empty will allow anything that is not on the same origin @@ -1935,6 +1938,9 @@ preinstall_disabled = false # Update strategy for plugins. # Available options: "latest", "minor" update_strategy = minor +# Enable automatic updates for preinstalled plugins on startup. +# When enabled, preinstalled plugins without a pinned version will be updated to the latest version. +preinstall_auto_update = true #################################### Grafana Live ########################################## [live] @@ -2203,6 +2209,16 @@ resource_storage_type = "db" # Current key provider used for envelope encryption encryption_provider = secret_key.v1 +# These flags are required in on-prem installations for GitSync to work +# +# Whether to register the MT CRUD API +register_api_server = true +# Whether to create the MT secrets management database +run_secrets_db_migrations = true +# Whether to run the data key id migration. Requires that RunSecretsDBMigrations is also true. +run_data_key_migration = true + + [secrets_manager.encryption.secret_key.v1] # Used to encrypt data keys secret_key = SW2YcwTIb9zpOOhoPsMm diff --git a/conf/sample.ini b/conf/sample.ini index 5518290525e..ed0233c1a29 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -333,6 +333,10 @@ # Optional. Specifies an Application Insights endpoint URL where the endpoint string is wrapped in backticks ``. ;application_insights_endpoint_url = +# Optional, defaults to true. Configure automatic route tracking for single page applications in Application Insights. +# See https://learn.microsoft.com/en-us/azure/azure-monitor/app/application-insights-faq#is-there-a-way-to-see-fewer-events-per-transaction-when-i-use-the-application-insights-javascript-sdk +;application_insights_auto_route_tracking = + # Controls if the UI contains any links to user feedback forms ;feedback_links_enabled = true @@ -442,7 +446,6 @@ ;csrf_always_check = false # Comma-separated list of plugins ids that will be loaded inside the frontend sandbox -# Currently behind the feature flag pluginsFrontendSandbox ;enable_frontend_sandbox_for_plugins = # Comma-separated list of paths for POST/PUT URL in actions. Empty will allow anything that is not on the same origin @@ -2101,12 +2104,22 @@ default_datasource_uid = ###################################### Secrets Manager ###################################### [secrets_manager] -# Used for signing +# Current key provider used for envelope encryption +;encryption_provider = secret_key.v1 + +# These flags are required in on-prem installations for GitSync to work +# +# Whether to register the MT CRUD API +;register_api_server = true +# Whether to create the MT secrets management database +;run_secrets_db_migrations = true +# Whether to run the data key id migration. Requires that RunSecretsDBMigrations is also true. +;run_data_key_migration = true + +[secrets_manager.encryption.secret_key.v1] +# Used to encrypt data keys ;secret_key = SW2YcwTIb9zpOOhoPsMm -# Current key provider used for envelope encryption, default to static value specified by secret_key -;encryption_provider = secretKey.v1 -# List of configured key providers, space separated (Enterprise only): e.g., awskms.v1 azurekv.v1 -;available_encryption_providers = + ################################## Frontend development configuration ################################### # Warning! Any settings placed in this section will be available on `process.env.frontend_dev_{foo}` within frontend code diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index eaa8334926c..65dadfae463 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -18,7 +18,7 @@ We recommend using [Homebrew](https://brew.sh/) for installing any missing depen ``` brew install git brew install go -brew install node@22 +brew install node@24 ``` ### Windows diff --git a/devenv/dev-dashboards/annotations/multi-lane-annotations.json b/devenv/dev-dashboards/annotations/multi-lane-annotations.json new file mode 100644 index 00000000000..edd9c6ef757 --- /dev/null +++ b/devenv/dev-dashboards/annotations/multi-lane-annotations.json @@ -0,0 +1,1020 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "maxDataPoints": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "noise": 10, + "refId": "A", + "scenarioId": "random_walk", + "spread": 10 + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"fields\": [\n {\n \"name\": \"type\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\",\n \"Milestones\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"color\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\",\n \"#F2495C\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"time\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1720697881000,\n 1728580067000,\n 1730129182000,\n 1730734644000,\n 1732542118000,\n 1736426576000,\n 1736874473000,\n 1738676647000,\n 1740067713000,\n 1740684246000,\n 1743733609000,\n 1744034815000,\n 1745335229000,\n 1745936130000,\n 1746107356000,\n 1747055303000,\n 1747946736000,\n 1748446531000,\n 1750098297000,\n 1750963080000,\n 1750963096000,\n 1752067555000,\n 1752080606000,\n 1753125884000,\n 1754395568000,\n 1754407010000,\n 1756228450000,\n 1757947221000,\n 1759763050000,\n 1759924632000,\n 1761572602000\n ],\n \"type\": \"time\"\n },\n {\n \"name\": \"timeEnd\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1729081505000,\n 1730129307000,\n 1730838127000,\n 1732543887000,\n 1736528756000,\n 1736874600000,\n 1740064375000,\n 1740170732000,\n 1742397235000,\n 1743733611000,\n 1744038997000,\n 1745341972000,\n 1745936141000,\n 1746118905000,\n 1747055307000,\n 1747853613000,\n 1748446520000,\n 1750187872000,\n 1750963071000,\n 1751038503000,\n 1752067563000,\n 1752166243000,\n 1753396603000,\n 1754395592000,\n 1754407004000,\n 1756228445000,\n 1758134868000,\n 1759763044000,\n 1761572592000,\n 1761572593000,\n null\n ],\n \"type\": \"number\"\n },\n {\n \"name\": \"title\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"0.1.0\",\n \"1.0.2\",\n \"1.0.3\",\n \"1.0.4\",\n \"1.0.5\",\n \"1.0.6\",\n \"1.0.7\",\n \"1.0.8\",\n \"1.0.9\",\n \"1.0.10\",\n \"1.0.11\",\n \"1.0.12\",\n \"1.0.13\",\n \"1.0.14\",\n \"1.0.15\",\n \"1.0.16\",\n \"1.0.17\",\n \"1.0.18\",\n \"1.0.19\",\n \"1.0.20\",\n \"1.0.21\",\n \"1.0.22\",\n \"1.0.23\",\n \"1.0.24\",\n \"1.0.25\",\n \"1.0.26\",\n \"1.0.27\",\n \"1.0.28\",\n \"1.0.29\",\n \"1.0.30\",\n \"1.0.31\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"text\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n false\n ],\n \"type\": \"boolean\"\n },\n {\n \"name\": \"source\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n {\n \"datasource\": {\n \"type\": \"grafana-github-datasource\",\n \"uid\": \"feyypehpl45xcf\"\n },\n \"enable\": true,\n \"hide\": false,\n \"iconColor\": \"red\",\n \"mappings\": {\n \"text\": {\n \"source\": \"field\",\n \"value\": \"closed\"\n },\n \"time\": {\n \"source\": \"field\",\n \"value\": \"created_at\"\n },\n \"timeEnd\": {\n \"source\": \"field\",\n \"value\": \"closed_at\"\n },\n \"title\": {\n \"source\": \"field\",\n \"value\": \"title\"\n }\n },\n \"name\": \"Milestones\",\n \"target\": {\n \"options\": {\n \"query\": \"\"\n },\n \"owner\": \"grafana\",\n \"queryType\": \"Milestones\",\n \"refId\": \"Anno\",\n \"repository\": \"logs-drilldown\"\n }\n },\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"isRegion\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n true,\n false\n ],\n \"type\": \"boolean\"\n }\n ],\n \"length\": 31,\n \"meta\": {\n \"dataTopic\": \"annotations\"\n }\n },\n {\n \"fields\": [\n {\n \"name\": \"type\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Issues\",\n \"Issues\",\n \"Issues\",\n \"Issues\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"color\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"#FADE2A\",\n \"#FADE2A\",\n \"#FADE2A\",\n \"#FADE2A\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"time\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1760112753000,\n 1759930212000,\n 1759924899000,\n 1759850404000\n ],\n \"type\": \"time\"\n },\n {\n \"name\": \"timeEnd\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1760972421000,\n null,\n null,\n 1759926839000\n ],\n \"type\": \"number\"\n },\n {\n \"name\": \"title\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"[BUG]: Error when data source configured for multi tenant queries\",\n \"[FEAT]: Allow users without explore permissions to use logs drilldown\",\n \"Sync Logs panel and label/fields empty states\",\n \"fix: labels clear variable state not showing\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"text\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"[BUG]: Error when data source configured for multi tenant queries\",\n \"[FEAT]: Allow users without explore permissions to use logs drilldown\",\n \"Sync Logs panel and label/fields empty states\",\n \"fix: labels clear variable state not showing\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"tags\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n [\n \"bug\",\n \"needs-triage\"\n ],\n [\n \"enhancement\",\n \"needs-triage\"\n ],\n [\n \"needs-triage\"\n ],\n [\n \"bug\"\n ]\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"source\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n {\n \"datasource\": {\n \"type\": \"grafana-github-datasource\",\n \"uid\": \"feyypehpl45xcf\"\n },\n \"enable\": true,\n \"hide\": false,\n \"iconColor\": \"yellow\",\n \"mappings\": {\n \"tags\": {\n \"source\": \"field\",\n \"value\": \"labels\"\n },\n \"time\": {\n \"source\": \"field\",\n \"value\": \"created_at\"\n },\n \"timeEnd\": {\n \"source\": \"field\",\n \"value\": \"closed_at\"\n }\n },\n \"name\": \"Issues\",\n \"target\": {\n \"options\": {\n \"query\": \"\"\n },\n \"owner\": \"grafana\",\n \"queryType\": \"Issues\",\n \"refId\": \"Anno\",\n \"repository\": \"logs-drilldown\"\n }\n },\n null,\n null,\n null\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"isRegion\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n true,\n false,\n false,\n true\n ],\n \"type\": \"boolean\"\n }\n ],\n \"length\": 4,\n \"meta\": {\n \"dataTopic\": \"annotations\"\n }\n },\n {\n \"fields\": [\n {\n \"name\": \"type\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Pulls\",\n \"Pulls\",\n \"Pulls\",\n \"Pulls\",\n \"Pulls\",\n \"Pulls\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"color\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"#5794F2\",\n \"#5794F2\",\n \"#5794F2\",\n \"#5794F2\",\n \"#5794F2\",\n \"#5794F2\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"time\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1759941908000,\n 1759854730000,\n 1759852733000,\n 1759841416000,\n 1759786844000,\n 1759762979000\n ],\n \"type\": \"time\"\n },\n {\n \"name\": \"timeEnd\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1760110687000,\n 1759857566000,\n 1759926838000,\n 1759843529000,\n 1759848237000,\n 1759769543000\n ],\n \"type\": \"number\"\n },\n {\n \"name\": \"title\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"feat(EmptyLogs): add button to fix with assistant\",\n \"docs: Update troubleshooting page\",\n \"fix: unexpected clear variable behavior\",\n \"docs: update stale readme, fix docker install script\",\n \"docs: Update install and troubleshooting\",\n \"fix: fix RegExp.source removing flags, use toString instead\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"text\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"feat(EmptyLogs): add button to fix with assistant\",\n \"docs: Update troubleshooting page\",\n \"fix: unexpected clear variable behavior\",\n \"docs: update stale readme, fix docker install script\",\n \"docs: Update install and troubleshooting\",\n \"fix: fix RegExp.source removing flags, use toString instead\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"source\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n {\n \"datasource\": {\n \"type\": \"grafana-github-datasource\",\n \"uid\": \"feyypehpl45xcf\"\n },\n \"enable\": true,\n \"hide\": false,\n \"iconColor\": \"blue\",\n \"mappings\": {\n \"time\": {\n \"source\": \"field\",\n \"value\": \"created_at\"\n },\n \"timeEnd\": {\n \"source\": \"field\",\n \"value\": \"merged_at\"\n }\n },\n \"name\": \"Pulls\",\n \"target\": {\n \"options\": {\n \"query\": \"\",\n \"timeField\": 1\n },\n \"owner\": \"grafana\",\n \"queryType\": \"Pull_Requests\",\n \"refId\": \"Anno\",\n \"repository\": \"logs-drilldown\"\n }\n },\n null,\n null,\n null,\n null,\n null\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"isRegion\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n true,\n true,\n true,\n true,\n true,\n true\n ],\n \"type\": \"boolean\"\n }\n ],\n \"length\": 6,\n \"meta\": {\n \"dataTopic\": \"annotations\"\n }\n },\n {\n \"fields\": [\n {\n \"name\": \"type\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\",\n \"Commits\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"color\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\",\n \"#73BF69\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"time\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n 1760349982000,\n 1760110687000,\n 1759950041000,\n 1759948586000,\n 1759933371000,\n 1759926838000,\n 1759857566000,\n 1759850172000,\n 1759848237000,\n 1759843529000,\n 1759769543000,\n 1759762725000\n ],\n \"type\": \"time\"\n },\n {\n \"name\": \"title\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"Matias Chomicki\",\n \"Matias Chomicki\",\n \"github-actions[bot]\",\n \"Liza Detrick\",\n \"grafana-plugins-platform-bot[bot]\",\n \"Galen Kistler\",\n \"J Stickler\",\n \"Piotr Jamróz\",\n \"J Stickler\",\n \"Galen Kistler\",\n \"Galen Kistler\",\n \"Galen Kistler\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"text\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"feat(LogsListScene): add defaultDisplayedFields support (#1554)\\n\\n* feat(LogsListScene): add defaultDisplayedFields support\\n\\n* chore: update mock\\n\\n* fix(Table): omit unsupported field\\n\\n* fix(LogListScene): reset default displayed fields when switching viz\\n\\n* chore: lint\",\n \"feat(EmptyLogs): add button to fix with assistant (#1571)\\n\\n* feat(LogListScene): add error type to identify empty results\\n\\n* feat(LogsPanelError): use assistant to investigate no results\\n\\n* fix(Shoo): shoo\\n\\n* feat(EmbeddedLogs): customize empty logs prompt\\n\\n* feat(LogsPanelError): implement custom CTA\\n\\n* chore: spelling\\n\\n* chore: add unit test\\n\\n* chore: remove it.only\\n\\n* fix(variableHelpers): exclude primary label from variables to clear\\n\\n* feat(embedding): refactor options\\n\\n* feat(assistant): create service\\n\\n* feat(assistant): integrate with empty layout and no matching labels\\n\\n* chore: fix types\\n\\n* chore: update tests\\n\\n* chore: remove quotes\\n\\n* chore: prevent access to an undefined object\\n\\n* chore: move label to constants\\n\\n* Revert \\\"chore: move label to constants\\\"\\n\\nThis reverts commit fbdf96415e5b2af7a5cae4eec6e6d5a30bc05d1c.\\n\\n* chore: use name instead of label\\n\\n* Revert \\\"fix(Shoo): shoo\\\"\\n\\nThis reverts commit 75993f7a845b514cb5da9667e94a2f0a3c913f24.\\n\\n* chore: more unit tests\\n\\n* test(variableHelpers): test getVariablesThatCanBeCleared\",\n \"chore: bump @grafana/create-plugin configuration to 5.26.9 (#1559)\",\n \"feat(table): preferences (#1534)\",\n \"chore(version): bump version to v1.0.29\",\n \"fix: unexpected clear variable behavior (#1567)\\n\\n* fix: show clear ui when query contains clearable label filters\",\n \"docs: Update troubleshooting page (#1568)\",\n \"fix: validate primary label correctly (#1561)\\n\\n* fix: properly clear variables for legacy urls\",\n \"docs: Update install and troubleshooting (#1564)\\n\\nCo-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com>\",\n \"docs: update stale readme, fix docker install script (#1565)\\n\\n* docs: update stale readme\\n\\n* chore: fix docker script\",\n \"fix: fix RegExp.source removing flags, use toString instead (#1563)\\n\\n* fix: fix RegExp.source removing flags, use toString instead\",\n \"fix: stale urls (#1562)\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"id\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n \"cf08043276597abe16f4ae9dbe97bd988d31b7ed\",\n \"01e343dacde3b9fcd862ec2cb2b04fbea4eb6d63\",\n \"9e67734938b980a80496d4cdc60efe7dbc4ba74e\",\n \"924ebd235b6ea99d297d8e04e08ac8ea90a32968\",\n \"23b9b5dc0504a4c397b326562cb796cfdc0531df\",\n \"089fdfa26c7daad5543be1aae2377850348663c3\",\n \"437a568a7a7279c436a674c7b1b0d11a4971d47b\",\n \"ee12a20b32db26b82fe32ed9d5868bd2dde4a4d0\",\n \"657880b399682101616b3413e04c2b664d018ef3\",\n \"fc776d767ebf2a64fb9fad7ab0f479cdaf536cdb\",\n \"a9207d235af9b2f5a4915f072801128a41c4063b\",\n \"e2eae8b8f45dc685659689f85962b0f0a58f13b1\"\n ],\n \"type\": \"string\"\n },\n {\n \"name\": \"source\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n {\n \"datasource\": {\n \"type\": \"grafana-github-datasource\",\n \"uid\": \"feyypehpl45xcf\"\n },\n \"enable\": true,\n \"hide\": false,\n \"iconColor\": \"green\",\n \"mappings\": {\n \"text\": {\n \"source\": \"field\",\n \"value\": \"message\"\n },\n \"time\": {\n \"source\": \"field\",\n \"value\": \"committed_at\"\n },\n \"title\": {\n \"source\": \"field\",\n \"value\": \"author\"\n }\n },\n \"name\": \"Commits\",\n \"target\": {\n \"options\": {\n \"gitRef\": \"main\"\n },\n \"owner\": \"grafana\",\n \"queryType\": \"Commits\",\n \"refId\": \"Anno\",\n \"repository\": \"logs-drilldown\"\n }\n },\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null,\n null\n ],\n \"type\": \"other\"\n },\n {\n \"name\": \"isRegion\",\n \"config\": {\n \"custom\": {}\n },\n \"values\": [\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false,\n false\n ],\n \"type\": \"boolean\"\n }\n ],\n \"length\": 12,\n \"meta\": {\n \"dataTopic\": \"annotations\"\n }\n }\n]", + "refId": "B", + "scenarioId": "raw_frame" + } + ], + "title": "Time series", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "annotations": { + "multiLane": true + }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Time series (multi-lane)", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "candleStyle": "candles", + "colorStrategy": "open-close", + "colors": { + "down": "red", + "up": "green" + }, + "includeAllFields": false, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mode": "candles+volume", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Candlestick", + "type": "candlestick" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "annotations": { + "multiLane": true + }, + "candleStyle": "candles", + "colorStrategy": "open-close", + "colors": { + "down": "red", + "up": "green" + }, + "includeAllFields": false, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mode": "candles+volume", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Candlestick (multi-lane)", + "type": "candlestick" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 5, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "State timeline", + "type": "state-timeline" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 6, + "options": { + "alignValue": "left", + "annotations": { + "multiLane": true + }, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "State timeline (multi-lane)", + "type": "state-timeline" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 7, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Heatmap", + "type": "heatmap" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 8, + "options": { + "annotations": { + "multiLane": true + }, + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Heatmap (multi-lane)", + "type": "heatmap" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 9, + "options": { + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Status history", + "type": "status-history" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 10, + "options": { + "annotations": { + "multiLane": true + }, + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Status history (multi-lane)", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 11, + "maxDataPoints": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"type\": \"timeseries\"\n },\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"config\": {\n \"interval\": 604800000\n }\n },\n {\n \"name\": \"A-series\",\n \"type\": \"number\",\n \"labels\": {},\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1755870318505,\n 1756475118505,\n 1757079918505,\n 1757684718505,\n 1758289518505,\n 1758894318505,\n 1759499118505,\n 1760103918505,\n 1760708718505,\n 1761313518505\n ],\n [\n 49.57457814271496,\n 64.78808691382616,\n 94.88860442042386,\n 96.59132232810856,\n 58.57144477681538,\n 79.33618638515327,\n 89.64117713117561,\n 134.51905322565585,\n 122.83710544843791,\n 79.84039369237018\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"B\",\n \"name\": \"xymark\",\n \"meta\": {\n \"dataTopic\": \"annotations\"\n },\n \"fields\": [\n {\n \"name\": \"xMin\",\n \"type\": \"time\",\n \"config\": {}\n },\n {\n \"name\": \"xMax\",\n \"type\": \"time\",\n \"config\": {}\n },\n {\n \"name\": \"yMin\",\n \"type\": \"number\",\n \"config\": {}\n },\n {\n \"name\": \"yMax\",\n \"type\": \"number\",\n \"config\": {}\n },\n {\n \"name\": \"color\",\n \"type\": \"string\",\n \"config\": {}\n },\n {\n \"name\": \"fillOpacity\",\n \"type\": \"number\",\n \"config\": {}\n },\n {\n \"name\": \"lineWidth\",\n \"type\": \"number\",\n \"config\": {}\n },\n {\n \"name\": \"lineStyle\",\n \"type\": \"string\",\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1757684718505\n ],\n [\n 1758894318505\n ],\n [\n 70\n ],\n [\n 120\n ],\n [\n \"#f00\"\n ],\n [\n 0.1\n ],\n [\n 1\n ],\n [\n \"dash\"\n ]\n ]\n }\n }\n]", + "refId": "B", + "scenarioId": "raw_frame" + } + ], + "title": "xymark", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": ["gdev", "panel-tests", "graph-ng"], + "templating": { + "list": [] + }, + "time": { + "from": "2025-08-22T13:45:18.505Z", + "to": "2025-10-28T04:38:37.130Z" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Multi-lane annotations", + "uid": "ad7p5pj", + "version": 17 +} diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index 2eb527326e7..9cb204da48a 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -36,8 +36,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -93,13 +92,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -115,8 +113,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -172,13 +169,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -194,8 +190,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -251,13 +246,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -273,8 +267,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -330,13 +323,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -352,8 +344,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -409,13 +400,12 @@ "showThresholdMarkers": false, "sparkline": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -431,8 +421,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -488,13 +477,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -510,8 +498,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -567,13 +554,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -602,8 +588,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -659,13 +644,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -681,8 +665,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -738,13 +721,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -760,8 +742,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -817,13 +798,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -839,8 +819,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -896,13 +875,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "max": 100, "min": 1, @@ -931,8 +909,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -992,13 +969,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1010,8 +986,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1071,13 +1046,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1089,8 +1063,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1150,13 +1123,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1168,8 +1140,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1229,13 +1200,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1247,8 +1217,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1308,13 +1277,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1339,8 +1307,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1404,13 +1371,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1422,8 +1388,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1487,13 +1452,12 @@ "showThresholdMarkers": false, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1505,8 +1469,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { @@ -1570,13 +1533,12 @@ "showThresholdMarkers": true, "sparkline": false }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "alias": "1", "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "refId": "A", "scenarioId": "csv_metric_values", @@ -1601,8 +1563,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -1664,12 +1625,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 98, @@ -1687,8 +1647,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -1750,12 +1709,11 @@ "sparkline": true, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 98, @@ -1786,8 +1744,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -1850,12 +1807,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 8, @@ -1873,8 +1829,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -1938,12 +1893,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 12, @@ -1961,8 +1915,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -2024,12 +1977,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 100, @@ -2047,8 +1999,7 @@ }, { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "description": "", "fieldConfig": { @@ -2110,12 +2061,11 @@ "sparkline": false, "spotlight": true }, - "pluginVersion": "12.2.0-pre", + "pluginVersion": "13.0.0-pre", "targets": [ { "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" + "type": "grafana-testdata-datasource" }, "hide": false, "max": 100, diff --git a/devenv/dev-dashboards/panel-heatmap/heatmap-x.json b/devenv/dev-dashboards/panel-heatmap/heatmap-x.json index a43e78ddb69..f6bc6a51885 100644 --- a/devenv/dev-dashboards/panel-heatmap/heatmap-x.json +++ b/devenv/dev-dashboards/panel-heatmap/heatmap-x.json @@ -295,6 +295,71 @@ ], "title": "Cells heatmap", "type": "heatmap" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 7, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "show": true, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [], + "title": "No data", + "type": "heatmap" } ], "schemaVersion": 37, diff --git a/devenv/dev-dashboards/panel-status-history/status-history-thresholds-mappings.json b/devenv/dev-dashboards/panel-status-history/status-history-thresholds-mappings.json new file mode 100644 index 00000000000..44d4dda9522 --- /dev/null +++ b/devenv/dev-dashboards/panel-status-history/status-history-thresholds-mappings.json @@ -0,0 +1,1159 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 15116, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 11, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "default", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default absolute thresholds", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 8, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default percentage thresholds", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 10 + }, + { + "color": "#EAB839", + "value": 20 + }, + { + "color": "#6ED0E0", + "value": 30 + } + ] + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 3, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "override thresholds", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 9.9999 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 14.9999 + }, + "type": "range" + }, + { + "options": { + "from": 15, + "result": { + "color": "red", + "index": 2 + }, + "to": 24.9999 + }, + "type": "range" + }, + { + "options": { + "from": 25, + "result": { + "color": "blue", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 6, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.47, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "default value mappings", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 9.9999 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 14.9999 + }, + "type": "range" + }, + { + "options": { + "from": 15, + "result": { + "color": "red", + "index": 2 + }, + "to": 24.9999 + }, + "type": "range" + }, + { + "options": { + "from": 25, + "result": { + "color": "blue", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 7, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + } + ], + "title": "override value mappings", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n },\n {\n \"color\": \"#EAB839\",\n \"value\": 10\n },\n {\n \"color\": \"red\",\n \"value\": 15\n },\n {\n \"color\": \"#6ED0E0\",\n \"value\": 25\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000\n ],\n [\n 5,\n 10,\n 20,\n 30\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "field thresholds from data", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 9, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5,10,20,30,40" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "hide": false, + "max": 30, + "min": 0.01, + "noise": 30, + "refId": "B", + "scenarioId": "random_walk", + "startValue": 1 + } + ], + "title": "threshold from random walk", + "transformations": [ + { + "id": "configFromData", + "options": { + "configRefId": "B", + "mappings": [ + { + "fieldName": "B-series", + "handlerKey": "threshold1" + } + ] + } + } + ], + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "color": "purple", + "index": 0, + "text": "null" + } + }, + "type": "special" + }, + { + "options": { + "match": "nan", + "result": { + "color": "red", + "index": 1, + "text": "NaN" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 18 + }, + "id": 12, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000\n ],\n [\n 5,\n null,\n 20,\n null,\n 40\n ]\n ],\n \"entities\": [null, { \"NaN\": [3]}]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "special null | NaN value mapping from data", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "match": "null+nan", + "result": { + "color": "super-light-red", + "index": 0, + "text": "null + NaN" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 18 + }, + "id": 13, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000\n ],\n [\n 5,\n null,\n 20,\n null,\n 40\n ]\n ],\n \"entities\": [null, { \"NaN\": [1]}]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "special null + NaN value mapping from data", + "type": "status-history" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "match": "false", + "result": { + "color": "red", + "index": 0 + } + }, + "type": "special" + }, + { + "options": { + "match": "null+nan", + "result": { + "color": "blue", + "index": 1, + "text": "null + NaN" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 18 + }, + "id": 14, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000,\n 1674750835000,\n 1674754235000,\n 1674757835000\n ],\n [\n null,\n null,\n false,\n true,\n true,\n false,\n true,\n null\n ]\n ],\n \"entities\": [null, { \"NaN\": [0], \"Undefined\": [1]}]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "boolean values from data", + "type": "status-history" + }, + { + "id": 15, + "type": "status-history", + "title": "no data", + "gridPos": { + "x": 18, + "y": 18, + "h": 9, + "w": 6 + }, + "fieldConfig": { + "defaults": { + "custom": { + "lineWidth": 0, + "fillOpacity": 70, + "spanNulls": false, + "insertNulls": false, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "axisPlacement": "auto" + }, + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "fieldMinMax": false + }, + "overrides": [] + }, + "pluginVersion": "12.3.0-pre", + "targets": [ + { + "scenarioId": "random_walk", + "refId": "A", + "seriesCount": 0 + } + ], + "datasource": { + "type": "grafana-testdata-datasource" + }, + "options": { + "mergeValues": true, + "showValue": "auto", + "alignValue": "center", + "rowHeight": 0.9, + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none", + "hideZeros": false + } + } + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": ["gdev", "panel-tests", "state-timeline", "graph-ng"], + "templating": { + "list": [] + }, + "time": { + "from": "2023-01-26T11:29:47.180Z", + "to": "2023-01-26T16:29:39.205Z" + }, + "timepicker": {}, + "timezone": "utc", + "title": "StatusHistory - Thresholds & Mappings", + "uid": "a2f4ad9e-3b44-4624-8067-35f31be5d309", + "version": 1 +} diff --git a/devenv/dev-dashboards/panel-table/table_sparkline_cell.json b/devenv/dev-dashboards/panel-table/table_sparkline_cell.json index 3337c794279..4725580cf0e 100644 --- a/devenv/dev-dashboards/panel-table/table_sparkline_cell.json +++ b/devenv/dev-dashboards/panel-table/table_sparkline_cell.json @@ -204,7 +204,27 @@ "value": 80 } ] - } + }, + "actions": [ + { + "fetch": { + "body": "{}", + "headers": [["Content-Type", "application/json"]], + "method": "GET", + "queryParams": [], + "url": "/api/health" + }, + "title": "Get instance health", + "type": "fetch" + } + ], + "links": [ + { + "targetBlank": true, + "title": "Google Grafana", + "url": "https://google.com/search?q=grafana" + } + ] }, "overrides": [] }, diff --git a/devenv/frontend-service/configs/nginx.conf b/devenv/frontend-service/configs/nginx.conf index bb136f695aa..f052bb52d17 100644 --- a/devenv/frontend-service/configs/nginx.conf +++ b/devenv/frontend-service/configs/nginx.conf @@ -30,7 +30,7 @@ server { server_name _; otel_trace on; - otel_trace_context inject; + otel_trace_context propagate; otel_span_name "$request_method Request"; location ~ ^/-/down/?$ { diff --git a/devenv/frontend-service/docker-compose.yaml b/devenv/frontend-service/docker-compose.yaml index 0d65b27c250..74ff93c2dd4 100644 --- a/devenv/frontend-service/docker-compose.yaml +++ b/devenv/frontend-service/docker-compose.yaml @@ -75,7 +75,7 @@ services: GF_TRACING_OPENTELEMETRY_OTLP_PROPAGATION: jaeger,w3c postgres: - image: postgres:16.1-alpine3.19 + image: postgres:16.1-alpine3.19@sha256:17eb369d9330fe7fbdb2f705418c18823d66322584c77c2b43cc0e1851d01de7 environment: POSTGRES_USER: grafana POSTGRES_PASSWORD: grafana @@ -86,7 +86,7 @@ services: - 'alloy.logs=true' alloy: - image: grafana/alloy:latest + image: grafana/alloy:v1.11.2@sha256:6ab34b8201f0e8b0c4346be4934c9965723af3f7f21dd9a65fd73f270f69b451 volumes: - ./configs/alloy:/alloy-config - /var/run/docker.sock:/var/run/docker.sock # To scrape Docker container logs @@ -104,7 +104,7 @@ services: - 'alloy.logs=true' prometheus: - image: prom/prometheus + image: prom/prometheus:v3.7.2@sha256:23031bfe0e74a13004252caaa74eccd0d62b6c6e7a04711d5b8bf5b7e113adc7 volumes: - prometheus-data:/prometheus command: @@ -116,7 +116,7 @@ services: - 'alloy.logs=true' loki: - image: grafana/loki + image: grafana/loki:3.5.7@sha256:0eaee7bf39cc83aaef46914fb58f287d4f4c4be6ec96b86c2ed55719a75e49c8 volumes: - loki-data:/loki command: -config.file=/etc/loki/local-config.yaml @@ -124,7 +124,7 @@ services: - 'alloy.logs=true' tempo-init: - image: busybox + image: busybox:1.37.0@sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee user: root entrypoint: - 'chown' @@ -134,7 +134,7 @@ services: - tempo-data:/var/tempo tempo: - image: grafana/tempo + image: grafana/tempo:2.9.0@sha256:65a5789759435f1ef696f1953258b9bbdb18eb571d5ce711ff812d2e128288a4 volumes: - tempo-data:/var/lib/tempo - ./configs/tempo.yaml:/etc/tempo/tempo.yaml diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index 80be668ecc4..cac117cd4e7 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -74,6 +74,7 @@ "mostly-blank-dashboard": (import '../dev-dashboards/scenarios/mostly-blank-dashboard.json'), "mssql_fakedata": (import '../dev-dashboards/datasource-mssql/mssql_fakedata.json'), "mssql_unittest": (import '../dev-dashboards/datasource-mssql/mssql_unittest.json'), + "multi-lane-annotations": (import '../dev-dashboards/annotations/multi-lane-annotations.json'), "mysql_fakedata": (import '../dev-dashboards/datasource-mysql/mysql_fakedata.json'), "mysql_unittest": (import '../dev-dashboards/datasource-mysql/mysql_unittest.json'), "new_features_in_v74": (import '../dev-dashboards/datasource-testdata/new_features_in_v74.json'), @@ -96,6 +97,7 @@ "rows-to-fields": (import '../dev-dashboards/transforms/rows-to-fields.json'), "shared_queries": (import '../dev-dashboards/panel-common/shared_queries.json'), "slow_queries_and_annotations": (import '../dev-dashboards/scenarios/slow_queries_and_annotations.json'), + "status-history-thresholds-mappings": (import '../dev-dashboards/panel-status-history/status-history-thresholds-mappings.json'), "table_footer": (import '../dev-dashboards/panel-table/table_footer.json'), "table_kitchen_sink": (import '../dev-dashboards/panel-table/table_kitchen_sink.json'), "table_markdown": (import '../dev-dashboards/panel-table/table_markdown.json'), diff --git a/devenv/scopes/README.md b/devenv/scopes/README.md new file mode 100644 index 00000000000..3607b70d3eb --- /dev/null +++ b/devenv/scopes/README.md @@ -0,0 +1,140 @@ +# Scopes Provisioning Script + +This script generates Scopes, ScopeNodes, and ScopeNavigations for Grafana development environments. + +## Usage + +### Create resources + +```bash +# From devenv directory +./setup.sh scopes + +# Or run directly +cd scopes +go run scopes.go +``` + +### Delete all gdev-prefixed resources + +```bash +# From devenv directory +./setup.sh undev + +# Or run directly +cd scopes +go run scopes.go -clean +``` + +**Note about caching**: The `/find/scope_navigations` endpoint used by the UI caches ScopeNavigation results for 15 minutes. After running cleanup, deleted resources may still appear in the UI until the cache expires. The resources are actually deleted (you can verify by checking the `/scopenavigations` list endpoint), but the UI will refresh after ~15 minutes or after restarting Grafana. + +Doing an `Empty Cache and Hard Reload` will also help. + +## Configuration + +The script reads from `scopes-config.yaml` by default. You can specify a different config file: + +```bash +go run scopes.go -config=my-config.yaml +``` + +### Configuration Format + +The configuration file uses YAML format with a natural tree structure. The indentation itself represents the hierarchy: + +- **scopes**: Map of scope definitions (key is the scope name) +- **tree**: Tree structure of scope nodes where the YAML structure defines parent-child relationships +- **navigations**: Map of scope navigations linking URLs to scopes (key is the navigation name) + +Example: + +```yaml +scopes: + app1: + title: Application 1 + filters: + - key: app + operator: equals + value: app1 + +tree: + environments: + title: Environments + nodeType: container + children: + production: + title: Production + nodeType: container + children: + app1-prod: + title: Application 1 + nodeType: leaf + linkId: app1 + linkType: scope + +navigations: + # Link to a dashboard + app1-nav: + url: /d/86Js1xRmk + scope: app1 + + # Link to another dashboard + app2-nav: + url: /d/GlAqcPgmz + scope: app2 + + # Custom URLs + explore-nav: + url: /explore + scope: app1 +``` + +### Tree Structure + +The tree structure uses YAML's natural indentation to represent hierarchy: + +- **Key**: Unique identifier for the node (will be prefixed with "gdev-") +- **title**: Display title +- **nodeType**: Either "container" (can have children) or "leaf" (selectable scope) +- **linkId**: References a scope name (if nodeType is "leaf") +- **linkType**: Usually "scope" +- **children**: Map of child nodes (nested structure follows YAML indentation) + +### Node Types + +- **container**: A category/grouping node that can contain other nodes +- **leaf**: A selectable node that links to a scope + +### Navigations + +Navigations link URLs to scopes. The `url` field should contain the full URL path (e.g., `/d/abc123` for dashboards or `/explore` for other pages). + +To find dashboard UIDs from gdev dashboards: + +```bash +# Find UIDs of all gdev dashboards +find devenv/dev-dashboards -name "*.json" -exec sh -c 'echo "{}:" && jq -r ".uid // .dashboard.uid // \"NO_UID\"" {}' \; + +# Or for a specific dashboard +jq -r ".uid // .dashboard.uid" devenv/dev-dashboards/all-panels.json +``` + +## Environment Variables + +- `GRAFANA_URL`: Grafana URL (default: http://localhost:3000) +- `GRAFANA_NAMESPACE`: Namespace (default: default) +- `GRAFANA_USER`: Grafana username (default: admin) +- `GRAFANA_PASSWORD`: Grafana password (default: admin) + +## Command Line Flags + +- `-url`: Grafana URL +- `-namespace`: Namespace +- `-config`: Config file path (default: scopes-config.yaml) +- `-user`: Grafana username +- `-password`: Grafana password +- `-clean`: Delete all gdev-prefixed resources + +## Prefix + +All resources are automatically prefixed with "gdev-" to avoid conflicts with production data. diff --git a/devenv/scopes/scopes-config.yaml b/devenv/scopes/scopes-config.yaml new file mode 100644 index 00000000000..46592742bbd --- /dev/null +++ b/devenv/scopes/scopes-config.yaml @@ -0,0 +1,84 @@ +scopes: + app1: + title: Application 1 + filters: + - key: app + operator: equals + value: app1 + + app2: + title: Application 2 + filters: + - key: app + operator: equals + value: app2 + + cluster1: + title: Cluster 1 + filters: + - key: cluster + operator: equals + value: cluster1 + +tree: + gdev-scopes: + title: gdev-scopes + nodeType: container + children: + production: + title: Production + nodeType: container + children: + app1-prod: + title: Application 1 + nodeType: leaf + linkId: app1 + linkType: scope + app2-prod: + title: Application 2 + nodeType: leaf + linkId: app2 + linkType: scope + test-cases: + title: Test cases + nodeType: container + disableMultiSelect: true + children: + test-case-1: + title: Test case 1 + nodeType: leaf + linkId: test-case-1 + linkType: scope + test-case-2: + title: Test case 2 + nodeType: leaf + linkId: test-case-2 + linkType: scope + + clusters: + title: Clusters + nodeType: container + linkId: cluster1 + linkType: scope + children: + cluster1-node: + title: Cluster 1 + nodeType: leaf + linkId: cluster1 + linkType: scope + +navigations: + # Example: Link to a dashboard + app1-nav: + url: /d/86Js1xRmk + scope: app1 + + # Example: Link to a dashboard with full URL (already has /d/) + app2-nav: + url: /d/GlAqcPgmz + scope: app2 + + # Example: Custom URL path + custom-nav: + url: /explore + scope: app1 diff --git a/devenv/scopes/scopes.go b/devenv/scopes/scopes.go new file mode 100644 index 00000000000..0480ee221e7 --- /dev/null +++ b/devenv/scopes/scopes.go @@ -0,0 +1,433 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "strings" + + "gopkg.in/yaml.v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1" +) + +const ( + prefix = "gdev" + apiVersion = "scope.grafana.app/v0alpha1" + defaultURL = "http://localhost:3000" + defaultUser = "admin" +) + +var ( + grafanaURL = flag.String("url", getEnv("GRAFANA_URL", defaultURL), "Grafana URL") + namespace = flag.String("namespace", getEnv("GRAFANA_NAMESPACE", "default"), "Namespace") + configFile = flag.String("config", "scopes-config.yaml", "Config file path") + user = flag.String("user", getEnv("GRAFANA_USER", defaultUser), "Grafana username") + password = flag.String("password", getEnv("GRAFANA_PASSWORD", "admin"), "Grafana password") + cleanupFlag = flag.Bool("clean", false, "Delete all gdev-prefixed resources") +) + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +type Config struct { + Scopes map[string]ScopeConfig `yaml:"scopes"` + Tree map[string]TreeNode `yaml:"tree"` + Navigations map[string]NavigationConfig `yaml:"navigations"` +} + +// ScopeConfig is used for YAML parsing - converts to v0alpha1.ScopeSpec +type ScopeConfig struct { + Title string `yaml:"title"` + Filters []ScopeFilterConfig `yaml:"filters"` +} + +// ScopeFilterConfig is used for YAML parsing - converts to v0alpha1.ScopeFilter +type ScopeFilterConfig struct { + Key string `yaml:"key"` + Value string `yaml:"value"` + Values []string `yaml:"values,omitempty"` + Operator string `yaml:"operator"` +} + +// TreeNode is used for YAML parsing - converts to v0alpha1.ScopeNodeSpec +type TreeNode struct { + Title string `yaml:"title"` + NodeType string `yaml:"nodeType"` + LinkID string `yaml:"linkId,omitempty"` + LinkType string `yaml:"linkType,omitempty"` + Children map[string]TreeNode `yaml:"children,omitempty"` +} + +type NavigationConfig struct { + URL string `yaml:"url"` // URL path (e.g., /d/abc123 or /explore) + Scope string `yaml:"scope"` +} + +// Helper function to convert ScopeFilterConfig to v0alpha1.ScopeFilter +func convertFilter(cfg ScopeFilterConfig) v0alpha1.ScopeFilter { + filter := v0alpha1.ScopeFilter{ + Key: cfg.Key, + Value: cfg.Value, + Values: cfg.Values, + Operator: v0alpha1.FilterOperator(cfg.Operator), + } + return filter +} + +// Helper function to convert ScopeConfig to v0alpha1.ScopeSpec +func convertScopeSpec(cfg ScopeConfig) v0alpha1.ScopeSpec { + filters := make([]v0alpha1.ScopeFilter, len(cfg.Filters)) + for i, f := range cfg.Filters { + filters[i] = convertFilter(f) + } + return v0alpha1.ScopeSpec{ + Title: cfg.Title, + Filters: filters, + } +} + +type Client struct { + baseURL string + namespace string + httpClient *http.Client + auth string +} + +func NewClient(baseURL, namespace, user, password string) *Client { + return &Client{ + baseURL: baseURL, + namespace: namespace, + httpClient: &http.Client{}, + auth: basicAuth(user, password), + } +} + +func basicAuth(username, password string) string { + return fmt.Sprintf("%s:%s", username, password) +} + +func (c *Client) makeRequest(method, endpoint string, body []byte) error { + url := fmt.Sprintf("%s/apis/%s/namespaces/%s%s", c.baseURL, apiVersion, c.namespace, endpoint) + + var req *http.Request + var err error + + if body != nil { + req, err = http.NewRequest(method, url, bytes.NewBuffer(body)) + } else { + req, err = http.NewRequest(method, url, nil) + } + + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth(strings.Split(c.auth, ":")[0], strings.Split(c.auth, ":")[1]) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + bodyBytes, _ := io.ReadAll(resp.Body) + // For DELETE requests, 404 is acceptable (resource already deleted) + if resp.StatusCode == 404 { + return nil + } + return fmt.Errorf("API request failed: HTTP %d - %s", resp.StatusCode, string(bodyBytes)) + } + + return nil +} + +func (c *Client) createScope(name string, cfg ScopeConfig) error { + prefixedName := prefix + "-" + name + + spec := convertScopeSpec(cfg) + + resource := v0alpha1.Scope{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiVersion, + Kind: "Scope", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: prefixedName, + }, + Spec: spec, + } + + body, err := json.Marshal(resource) + if err != nil { + return fmt.Errorf("failed to marshal scope: %w", err) + } + + fmt.Printf("✓ Creating scope: %s\n", prefixedName) + return c.makeRequest("POST", "/scopes", body) +} + +func (c *Client) createScopeNode(name string, node TreeNode, parentName string) error { + prefixedName := prefix + "-" + name + prefixedParent := "" + prefixedLinkID := "" + + if parentName != "" { + prefixedParent = prefix + "-" + parentName + } + + if node.LinkID != "" { + prefixedLinkID = prefix + "-" + node.LinkID + } + + nodeType := v0alpha1.NodeType(node.NodeType) + if nodeType == "" { + nodeType = v0alpha1.NodeTypeContainer + } + + linkType := v0alpha1.LinkType(node.LinkType) + if linkType == "" { + linkType = v0alpha1.LinkTypeScope + } + + spec := v0alpha1.ScopeNodeSpec{ + Title: node.Title, + NodeType: nodeType, + DisableMultiSelect: false, + } + + if prefixedParent != "" { + spec.ParentName = prefixedParent + } + + if prefixedLinkID != "" { + spec.LinkID = prefixedLinkID + spec.LinkType = linkType + } + + resource := v0alpha1.ScopeNode{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiVersion, + Kind: "ScopeNode", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: prefixedName, + }, + Spec: spec, + } + + body, err := json.Marshal(resource) + if err != nil { + return fmt.Errorf("failed to marshal scope node: %w", err) + } + + fmt.Printf("✓ Creating scope node: %s\n", prefixedName) + return c.makeRequest("POST", "/scopenodes", body) +} + +func (c *Client) createScopeNavigation(name string, nav NavigationConfig) error { + prefixedName := prefix + "-" + name + prefixedScope := prefix + "-" + nav.Scope + + if nav.URL == "" { + return fmt.Errorf("navigation %s must have 'url' specified", name) + } + + spec := v0alpha1.ScopeNavigationSpec{ + URL: nav.URL, + Scope: prefixedScope, + } + + resource := v0alpha1.ScopeNavigation{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiVersion, + Kind: "ScopeNavigation", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: prefixedName, + }, + Spec: spec, + } + + body, err := json.Marshal(resource) + if err != nil { + return fmt.Errorf("failed to marshal scope navigation: %w", err) + } + + fmt.Printf("✓ Creating scope navigation: %s\n", prefixedName) + return c.makeRequest("POST", "/scopenavigations", body) +} + +func (c *Client) createTreeNodes(children map[string]TreeNode, parentName string) error { + for name, node := range children { + // Build full node name by appending to parent name + // This makes it easy to see the tree path from the node name + fullNodeName := name + if parentName != "" { + fullNodeName = parentName + "-" + name + } + + // parentName here is the full parent name (already includes full path) + err := c.createScopeNode(fullNodeName, node, parentName) + if err != nil { + return err + } + + if len(node.Children) > 0 { + // Pass fullNodeName as parent for children (will be prefixed with "gdev-" in createScopeNode) + if err := c.createTreeNodes(node.Children, fullNodeName); err != nil { + return err + } + } + } + + return nil +} + +func (c *Client) deleteResources() { + fmt.Println("Deleting all gdev-prefixed resources...") + + // Delete scopes (silently handle errors if endpoints aren't available) + c.deleteResourceType("/scopes", "scope") + + // Delete scope nodes + c.deleteResourceType("/scopenodes", "scope node") + + // Delete scope navigations + c.deleteResourceType("/scopenavigations", "scope navigation") + + fmt.Println("✓ Cleanup complete") +} + +func (c *Client) deleteResourceType(endpoint, resourceType string) { + url := fmt.Sprintf("%s/apis/%s/namespaces/%s%s", c.baseURL, apiVersion, c.namespace, endpoint) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + // Silently skip if we can't create request + return + } + + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth(strings.Split(c.auth, ":")[0], strings.Split(c.auth, ":")[1]) + + resp, err := c.httpClient.Do(req) + if err != nil { + // Silently skip if endpoint isn't available + return + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + // Silently skip if endpoint returns error (might not be available) + return + } + + var listResponse struct { + Items []struct { + Metadata struct { + Name string `json:"name"` + } `json:"metadata"` + } `json:"items"` + } + + bodyBytes, _ := io.ReadAll(resp.Body) + if err := json.Unmarshal(bodyBytes, &listResponse); err != nil { + // Silently skip if we can't decode response + return + } + + if len(listResponse.Items) == 0 { + return + } + + deletedCount := 0 + for _, item := range listResponse.Items { + if strings.HasPrefix(item.Metadata.Name, prefix+"-") { + fmt.Printf(" Deleting %s: %s\n", resourceType, item.Metadata.Name) + deleteURL := fmt.Sprintf("%s/%s", endpoint, item.Metadata.Name) + if err := c.makeRequest("DELETE", deleteURL, nil); err != nil { + // Silently skip deletion errors + } else { + deletedCount++ + } + } + } +} + +func main() { + flag.Parse() + + client := NewClient(*grafanaURL, *namespace, *user, *password) + + if *cleanupFlag { + // Cleanup should be silent if endpoints aren't available + client.deleteResources() + return + } + + configData, err := os.ReadFile(*configFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err) + os.Exit(1) + } + + var config Config + if err := yaml.Unmarshal(configData, &config); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing config file: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Loading configuration from: %s\n", *configFile) + fmt.Printf("Grafana URL: %s\n", *grafanaURL) + fmt.Printf("Namespace: %s\n", *namespace) + fmt.Printf("Prefix: %s\n\n", prefix) + + // Create scopes + fmt.Println("Creating scopes...") + for name, scope := range config.Scopes { + if err := client.createScope(name, scope); err != nil { + fmt.Fprintf(os.Stderr, "Error creating scope %s: %v\n", name, err) + os.Exit(1) + } + } + fmt.Println() + + // Create scope nodes (tree structure) + if len(config.Tree) > 0 { + fmt.Println("Creating scope nodes...") + if err := client.createTreeNodes(config.Tree, ""); err != nil { + fmt.Fprintf(os.Stderr, "Error creating scope nodes: %v\n", err) + os.Exit(1) + } + fmt.Println() + } + + // Create scope navigations + if len(config.Navigations) > 0 { + fmt.Println("Creating scope navigations...") + for name, nav := range config.Navigations { + if err := client.createScopeNavigation(name, nav); err != nil { + fmt.Fprintf(os.Stderr, "Error creating scope navigation %s: %v\n", name, err) + os.Exit(1) + } + } + fmt.Println() + } + + fmt.Println("✓ All resources created successfully!") +} diff --git a/devenv/setup.sh b/devenv/setup.sh index 00eb50b5257..d3774ca19ec 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -19,6 +19,13 @@ bulkFolders() { ln -s -f ../../../devenv/bulk-folders/bulk-folders.yaml ../conf/provisioning/dashboards/bulk-folders.yaml } +scopes() { + echo -e "\xE2\x9C\x94 Setting up scopes, scope nodes, and scope navigations" + cd scopes + go run scopes.go + cd .. +} + requiresJsonnet() { if ! type "jsonnet" > /dev/null; then echo "you need you install jsonnet to run this script" @@ -49,6 +56,12 @@ undev() { rm -rf bulk-folders/Bulk\ Folder* echo -e " \xE2\x9C\x94 Reverting bulk-folders provisioning" + # Removing scopes, scope nodes, and scope navigations + cd scopes + go run scopes.go -clean + cd .. + echo -e " \xE2\x9C\x94 Deleting scopes, scope nodes, and scope navigations" + # Removing the symlinks rm -f ../conf/provisioning/dashboards/custom.yaml rm -f ../conf/provisioning/dashboards/bulk-folders.yaml @@ -63,6 +76,7 @@ usage() { echo " bulk-dashboards - provision 400 dashboards" echo " bulk-folders [folders] [dashboards] - provision many folders with dashboards" echo " bulk-folders - provision 200 folders with 3 dashboards in each" + echo " scopes - provision scopes, scope nodes, and scope navigations" echo " no args - provision core datasources and dev dashboards" echo " undev - removes any provisioning done by the setup.sh" } @@ -80,6 +94,8 @@ main() { bulkDashboard elif [[ $cmd == "bulk-folders" ]]; then bulkFolders "$arg1" + elif [[ $cmd == "scopes" ]]; then + scopes elif [[ $cmd == "undev" ]]; then undev else diff --git a/docs/sources/administration/announcement-banner/_index.md b/docs/sources/administration/announcement-banner/_index.md index b67e0d7ca1d..020ca45c877 100644 --- a/docs/sources/administration/announcement-banner/_index.md +++ b/docs/sources/administration/announcement-banner/_index.md @@ -9,6 +9,7 @@ labels: menuTitle: Announcement banner title: Create and configure announcement banner description: How to create an announcement banner to show important updates and information at the top of every Grafana page. +weight: 5500 --- # Create an announcement banner diff --git a/docs/sources/administration/back-up-grafana/index.md b/docs/sources/administration/back-up-grafana/index.md index 2c9286f0b0e..0ceb0b4e5fc 100644 --- a/docs/sources/administration/back-up-grafana/index.md +++ b/docs/sources/administration/back-up-grafana/index.md @@ -8,7 +8,7 @@ labels: - enterprise - oss title: Back up Grafana -weight: 80 +weight: 100 menuTitle: Back up Grafana --- diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index 0e8e6e611e0..416b1c24335 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -1,45 +1,43 @@ --- aliases: - ../cli/ # /docs/grafana/latest/cli/ -description: Guide to using grafana server cli +description: Guide to using the Grafana server CLI keywords: - grafana - cli - grafana cli - command line interface + - admin labels: products: - enterprise - oss title: Grafana server CLI -weight: 1100 +menuTitle: Admin with Grafana server CLI +weight: 4000 --- -# Grafana server CLI +# Administer Grafana with the Grafana server CLI -Grafana server CLI is a small executable that's bundled with Grafana server. -You can run it on the same machine Grafana server is running on. -Grafana server CLI has `plugins` and `admin` commands, as well as global options. +You can administer your Grafana instance with the Grafana server CLI, a small executable bundled with Grafana server. -To list all commands and options: +The Grafana server CLI has `plugins` and `admin` commands, as well as global options. To list them, run: ``` grafana cli -h ``` -## Run Grafana server CLI +For more details read on. -To run Grafana server CLI, add the path to the Grafana binaries in your `PATH` environment variable. -Alternately, if your current directory is the `bin` directory, run `./grafana cli`. -Otherwise, you can specify full path to the binary. -For example, on Linux `/usr/share/grafana/bin/grafana` and on Windows `C:\Program Files\GrafanaLabs\grafana\bin\grafana.exe`, and run it with `grafana cli`. +## Run the Grafana server CLI -{{< admonition type="note" >}} -Some commands, such as installing or removing plugins, require `sudo` on Linux. -If you're on Windows, run Windows PowerShell as Administrator. -{{< /admonition >}} +You can run the Grafana server CLI on the same machine Grafana server is running on. -## Grafana CLI command syntax +To run the CLI you have the following options: + +- Add the path to the Grafana binaries in your `PATH` environment variable. +- If your current directory is the `bin` directory, run `./grafana cli`. +- Otherwise, you can specify full path to the binary. For example, `/usr/share/grafana/bin/grafana` on Linux and `C:\Program Files\GrafanaLabs\grafana\bin\grafana.exe` on Windows. The general syntax for commands in Grafana server CLI is: @@ -47,6 +45,11 @@ The general syntax for commands in Grafana server CLI is: grafana cli [global options] command [command options] [arguments...] ``` +{{< admonition type="note" >}} +Some commands, such as installing or removing plugins, require `sudo` on Linux. +If you're on Windows, run Windows PowerShell as Administrator. +{{< /admonition >}} + ## Global options Grafana server CLI allows you to temporarily override certain Grafana default settings. Except for `--help` and `--version`, most global options are only used by developers. diff --git a/docs/sources/administration/correlations/_index.md b/docs/sources/administration/correlations/_index.md index 36e0badf5f5..7d449ce88b9 100644 --- a/docs/sources/administration/correlations/_index.md +++ b/docs/sources/administration/correlations/_index.md @@ -7,7 +7,7 @@ labels: - enterprise - oss title: Correlations -weight: 900 +weight: 6000 --- # Correlations diff --git a/docs/sources/administration/data-source-management/_index.md b/docs/sources/administration/data-source-management/_index.md index 1a28b0001c3..8cb0d57d30e 100644 --- a/docs/sources/administration/data-source-management/_index.md +++ b/docs/sources/administration/data-source-management/_index.md @@ -12,7 +12,7 @@ labels: - enterprise - cloud title: Data source management -weight: 100 +weight: 500 --- # Data source management diff --git a/docs/sources/administration/enterprise-licensing/_index.md b/docs/sources/administration/enterprise-licensing/_index.md index 11619aa43e7..759431ebad7 100644 --- a/docs/sources/administration/enterprise-licensing/_index.md +++ b/docs/sources/administration/enterprise-licensing/_index.md @@ -18,7 +18,7 @@ labels: - enterprise - oss title: Grafana Enterprise license -weight: 500 +weight: 5500 --- # Grafana Enterprise license diff --git a/docs/sources/administration/grafana-advisor/_index.md b/docs/sources/administration/grafana-advisor/_index.md index 38673e9e121..6292a13fd6b 100644 --- a/docs/sources/administration/grafana-advisor/_index.md +++ b/docs/sources/administration/grafana-advisor/_index.md @@ -1,7 +1,7 @@ --- title: Grafana Advisor description: Learn more about Grafana Advisor, the app to monitor the health of your Grafana instance -weight: 300 +weight: 700 labels: products: - oss diff --git a/docs/sources/administration/migration-guide/_index.md b/docs/sources/administration/migration-guide/_index.md index 94576db65c0..d601b95d331 100644 --- a/docs/sources/administration/migration-guide/_index.md +++ b/docs/sources/administration/migration-guide/_index.md @@ -9,6 +9,7 @@ keywords: - Grafana OSS menuTitle: Migrate from Grafana OSS/Enterprise to Grafana Cloud title: Migrate from Grafana OSS/Enterprise to Grafana Cloud +weight: 7000 --- # Migrate from Grafana OSS/Enterprise to Grafana Cloud diff --git a/docs/sources/administration/organization-management/index.md b/docs/sources/administration/organization-management/index.md index 7eef5d45424..621374d4884 100644 --- a/docs/sources/administration/organization-management/index.md +++ b/docs/sources/administration/organization-management/index.md @@ -14,7 +14,7 @@ labels: - oss menuTitle: Manage organizations title: Manage organizations -weight: 200 +weight: 3500 --- # Manage organizations diff --git a/docs/sources/administration/organization-preferences/index.md b/docs/sources/administration/organization-preferences/index.md index bc54894a3ab..0df51d324c7 100644 --- a/docs/sources/administration/organization-preferences/index.md +++ b/docs/sources/administration/organization-preferences/index.md @@ -11,7 +11,7 @@ labels: - enterprise - oss title: Organization preferences -weight: 500 +weight: 3600 --- # Organization preferences diff --git a/docs/sources/administration/plugin-management/_index.md b/docs/sources/administration/plugin-management/_index.md index fce84ec643e..6736e15d176 100644 --- a/docs/sources/administration/plugin-management/_index.md +++ b/docs/sources/administration/plugin-management/_index.md @@ -20,7 +20,7 @@ Plugins enhance your Grafana experience with new ways to connect to and visualiz Read on for an overview on how to get started with plugins: -- Plugins are available in the [plugin catalog](#plugin-catalog). They can be built by Grafana Labs, commercial partners, our community, or you can [build a plugin yourself](/developers/plugin-tools). +- Plugins are available in the [plugin catalog](#access-the-plugin-catalog). They can be built by Grafana Labs, commercial partners, our community, or you can [build a plugin yourself](/developers/plugin-tools). - There are three [types of plugins](#types-of-plugins): panel, data source, and app plugins. - Learn [how to install](#install-a-plugin), [update](#update-a-plugin) and [verify](#verify-your-plugins) your plugins. @@ -40,11 +40,11 @@ Grafana supports three types of plugins: Read more in [Types of plugins](plugin-types). -## Plugin catalog +## Access the Plugin catalog -The Grafana plugin catalog allows you to browse and manage plugins from within Grafana. Only Grafana server administrators and Organization administrators can access and use the plugin catalog. For more information about Grafana roles and permissions, refer to [Roles and permissions](../roles-and-permissions/). +You can install and manage plugins from within Grafana. You need to have a Grafana Server administrator or Organization administrator role to access and use the plugin catalog. For more information about Grafana roles and permissions, refer to [Roles and permissions](../roles-and-permissions/). -The following access rules apply depending on the user role: +For app plugins, the following access rules apply: - If you are an **Org Admin**, you can configure app plugins, but you can't install, uninstall, or update them. - If you are a **Server Admin**, you can't configure app plugins, but you can install, uninstall, or update them. @@ -58,6 +58,8 @@ To browse for available plugins: 1. Use the search box to filter based on name, keywords, organization and other metadata. 1. Click the **Data sources**, **Panels**, or **Applications** buttons to filter by plugin type. +If you're not logged in, you can also access the list of available plugins in the [Plugin catalog](https://grafana.com/grafana/plugins/). + ## Manage your plugins We strongly recommend running the latest plugin version. Use [Grafana Advisor](https://grafana.com/docs/grafana//administration/grafana-advisor) to check the status of your data sources and plugins. diff --git a/docs/sources/administration/plugin-management/plugin-frontend-sandbox.md b/docs/sources/administration/plugin-management/plugin-frontend-sandbox.md index 19c5c6f72b3..1b2721d49a8 100644 --- a/docs/sources/administration/plugin-management/plugin-frontend-sandbox.md +++ b/docs/sources/administration/plugin-management/plugin-frontend-sandbox.md @@ -53,11 +53,7 @@ The following applies: ## Enable the Frontend Sandbox -The Frontend Sandbox feature is currently behind the `pluginsFrontendSandbox` feature flag. To enable it, you need to: - -1. Enable the feature flag in your Grafana configuration. For more information about enabling feature flags, refer to [Configure feature toggles](/docs/grafana//setup-grafana/configure-grafana/feature-toggles/). - -2. For self-hosted Grafana installations, add the plugin IDs you want to sandbox in the `security` section using the `enable_frontend_sandbox_for_plugins` configuration option. +For self-hosted Grafana installations, add the plugin IDs you want to sandbox in the `security` section using the `enable_frontend_sandbox_for_plugins` configuration option. For Grafana Cloud users, you can simply use the toggle switch in the plugin catalog page to enable or disable the sandbox for each plugin. By default, the sandbox is disabled for all plugins. diff --git a/docs/sources/administration/provisioning/index.md b/docs/sources/administration/provisioning/index.md index 8874448224f..fadb936e5a0 100644 --- a/docs/sources/administration/provisioning/index.md +++ b/docs/sources/administration/provisioning/index.md @@ -11,7 +11,8 @@ labels: - enterprise - oss title: Provision Grafana -weight: 600 +menuTitle: Provision Grafana +weight: 4100 --- # Provision Grafana @@ -374,6 +375,58 @@ providers: ``` When Grafana starts, it updates or creates all dashboards found in the configured path. +These files can define the dashboard using the dashboard JSON: + +```json +{ + "dashboard": { + "id": null, + "uid": "example-dashboard", + "title": "Production Overview", + "tags": ["production", "monitoring"], + "timezone": "browser", + "schemaVersion": 16, + "version": 0, + "refresh": "30s" + }, + "folderUid": "monitoring-folder", + "overwrite": true +} +``` + +Or using a Kubernetes format, for example `kubernetes-dashboard.json`: + +```json +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "dashboard-uid" + }, + "spec": { + "title": "Dashboard title", + "panels": [ + { + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 0 + }, + "options": { + "content": "

Example panel

", + "mode": "html" + }, + "transparent": true, + "type": "text" + } + ] + } +} +``` + +You _must_ use the Kubernetes resource format to provision dashboards v2 / dynamic dashboards. + It later polls that path every `updateIntervalSeconds` for updates to the dashboard files and updates its database. {{< admonition type="note" >}} diff --git a/docs/sources/administration/recorded-queries/index.md b/docs/sources/administration/recorded-queries/index.md index 425ff2bbf7c..e76021ae8d7 100644 --- a/docs/sources/administration/recorded-queries/index.md +++ b/docs/sources/administration/recorded-queries/index.md @@ -11,11 +11,12 @@ labels: products: - cloud - enterprise -title: Recorded queries -weight: 300 +title: Recorded queries (deprecated) +menuTitle: Recorded queries (deprecated) +weight: 9000 --- -# DEPRECATED Recorded queries +# Recorded queries (deprecated) {{< admonition type="warning" >}} Recorded queries are deprecated. Please use the new [Grafana-managed recording rules](/docs/grafana/latest/alerting/alerting-rules/create-recording-rules/create-grafana-managed-recording-rules) instead. diff --git a/docs/sources/administration/roles-and-permissions/_index.md b/docs/sources/administration/roles-and-permissions/_index.md index d6cca1b7eb9..c8135836fa1 100644 --- a/docs/sources/administration/roles-and-permissions/_index.md +++ b/docs/sources/administration/roles-and-permissions/_index.md @@ -12,7 +12,7 @@ labels: - oss - cloud title: Roles and permissions -weight: 300 +weight: 3100 --- # Roles and permissions diff --git a/docs/sources/administration/search/_index.md b/docs/sources/administration/search/_index.md index 5bd777e9f01..7e41f995ad3 100644 --- a/docs/sources/administration/search/_index.md +++ b/docs/sources/administration/search/_index.md @@ -15,6 +15,7 @@ labels: - oss menutitle: Search title: Search +weight: 8000 --- # Grafana search diff --git a/docs/sources/administration/service-accounts/_index.md b/docs/sources/administration/service-accounts/_index.md index 5eb48e324b6..34bbd916610 100644 --- a/docs/sources/administration/service-accounts/_index.md +++ b/docs/sources/administration/service-accounts/_index.md @@ -15,7 +15,7 @@ labels: - cloud menuTitle: Service accounts title: Service accounts -weight: 800 +weight: 4200 refs: service-accounts: - pattern: /docs/grafana/ diff --git a/docs/sources/administration/stats-and-license/index.md b/docs/sources/administration/stats-and-license/index.md index ec926f73c2f..c8fe61fe47d 100644 --- a/docs/sources/administration/stats-and-license/index.md +++ b/docs/sources/administration/stats-and-license/index.md @@ -16,7 +16,8 @@ labels: - cloud - enterprise title: View server statistics and license -weight: 400 +menutitle: Server stats and license +weight: 5000 --- # View server statistics and license diff --git a/docs/sources/administration/team-management/_index.md b/docs/sources/administration/team-management/_index.md index 0a35b764cb7..5fff00428f3 100644 --- a/docs/sources/administration/team-management/_index.md +++ b/docs/sources/administration/team-management/_index.md @@ -15,11 +15,11 @@ keywords: - microservices - architecture menuTitle: Grafana Teams -title: Grafana Teams -weight: 100 +title: Manage teams with Grafana Teams +weight: 2000 --- -# Grafana Teams +# Manage teams with Grafana Teams Grafana Teams makes it easy to organize and administer groups of users in your enterprise. Teams allows you to grant permissions to a group of users instead of granting permissions to individual users one at a time. diff --git a/docs/sources/administration/user-management/_index.md b/docs/sources/administration/user-management/_index.md index ad3be03f7cb..3b6ff208903 100644 --- a/docs/sources/administration/user-management/_index.md +++ b/docs/sources/administration/user-management/_index.md @@ -5,8 +5,8 @@ labels: products: - enterprise - oss -title: User management -weight: 200 +title: Manage users +weight: 3000 --- # User management diff --git a/docs/sources/alerting/alerting-rules/alerting-migration.md b/docs/sources/alerting/alerting-rules/alerting-migration.md index 2e63db00136..3afa3aec453 100644 --- a/docs/sources/alerting/alerting-rules/alerting-migration.md +++ b/docs/sources/alerting/alerting-rules/alerting-migration.md @@ -65,6 +65,10 @@ The copied rules are converted to Grafana-managed rules, preserving their behavi The rule query offset is taken from the `query_offset` value in the rule group configuration. If empty, it defaults to the [`rule_query_offset` configuration setting](ref:configure-grafana-rule_query_offset), which is `1m` by default. +- **Rule query conversion** + + For alert rules, adds `prometheus_math` and `threshold` expressions to preserve Prometheus no data behavior, ensuring the alert stays in **Normal** state when `query` returns no data. + - **Missing series evaluations to resolve** The [Missing series evaluations to resolve](ref:missing_series_evaluations_to_resolve) setting is set to `1` to replicate Prometheus’s alert eviction behavior. diff --git a/docs/sources/alerting/alerting-rules/create-recording-rules/create-data-source-managed-recording-rules.md b/docs/sources/alerting/alerting-rules/create-recording-rules/create-data-source-managed-recording-rules.md index e8dd965ffff..ab14f760d65 100644 --- a/docs/sources/alerting/alerting-rules/create-recording-rules/create-data-source-managed-recording-rules.md +++ b/docs/sources/alerting/alerting-rules/create-recording-rules/create-data-source-managed-recording-rules.md @@ -47,7 +47,7 @@ Note that in data source-managed groups, the alert rules and recording rules wit To create a new data source-managed recording rule: 1. Click **Alerts & IRM** -> **Alerting** -> **Alert rules**. -1. At the top of the Alert rules page, click **More** -> **New Grafana recording rule**. +1. At the top of the Alert rules page, click **More** -> **New Data source recording rule**. ## Enter recording rule name diff --git a/docs/sources/alerting/alerting-rules/templates/reference.md b/docs/sources/alerting/alerting-rules/templates/reference.md index d638b8e1d3c..b80aed07766 100644 --- a/docs/sources/alerting/alerting-rules/templates/reference.md +++ b/docs/sources/alerting/alerting-rules/templates/reference.md @@ -141,6 +141,20 @@ Alternatively, you can use the `index()` function to retrieve the query value: {{ index $values "B" }} CPU usage for {{ index $labels "instance" }} over the last 5 minutes. ``` +{{< admonition type="note" >}} + +Variable names that start with a number (for example, `1B`) are not [valid identifiers in Go templates](https://go.dev/ref/spec#Identifiers). + +To access a value or label whose key starts with a number, use the `index` function: + +``` +{{ index $values "1B" }} CPU usage for {{ index $labels "1instance" }} over the last 5 minutes. +``` + +Using `{{ $values.1B.Value }}` is invalid and causes the template code to render as plain text. + +{{< /admonition >}} + #### $value 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. diff --git a/docs/sources/alerting/fundamentals/notifications/group-alert-notifications.md b/docs/sources/alerting/fundamentals/notifications/group-alert-notifications.md index 64b1e83ef54..91b5f217315 100644 --- a/docs/sources/alerting/fundamentals/notifications/group-alert-notifications.md +++ b/docs/sources/alerting/fundamentals/notifications/group-alert-notifications.md @@ -159,7 +159,9 @@ Once the first notification has been sent for a new group of alerts, the group i When the group interval timer elapses, the system resets the group interval timer and sends a notification only if there were group changes. This process repeats until there are no more alerts. -It's important to note that an alert instance exits the group after being resolved and notified of its state change. When no alerts remain, the group is deleted, and then the group wait timer handles the first notification for the next incoming alert once again. +It's important to note that an alert instance exits the group after being resolved and notified of its state change. + +When the group interval timer elapses and no alerts remain, the group is deleted. The [group wait timer](#group-wait) will then start again the next time a new alert arrives. ### Repeat interval diff --git a/docs/sources/alerting/set-up/performance-limitations/index.md b/docs/sources/alerting/set-up/performance-limitations/index.md index 82e8ccd41f1..b18f3b6ec68 100644 --- a/docs/sources/alerting/set-up/performance-limitations/index.md +++ b/docs/sources/alerting/set-up/performance-limitations/index.md @@ -68,7 +68,21 @@ You can change this behavior by disabling the `alertingSaveStateCompressed` feat You can also reduce database load by writing states periodically instead of after every evaluation. -To save state periodically: +There are two approaches for periodic state saving: + +#### Compressed periodic saves + +You can combine compressed alert state storage with periodic saves by enabling both `alertingSaveStateCompressed` and `alertingSaveStatePeriodic` feature toggles together. + +This approach groups all alert instances by rule UID and compresses them together for efficient storage. + +When both feature toggles are enabled, Grafana will save compressed alert states at the interval specified by `state_periodic_save_interval`. Note that in compressed mode, the `state_periodic_save_batch_size` setting is ignored as the system groups instances by rule UID rather than by batch size. + +#### Batch-based periodic saves + +Alternatively, you can use batch-based periodic saves without compression: + +This approach processes individual alert instances in batches of a specified size. 1. Enable the `alertingSaveStatePeriodic` feature toggle. 1. Disable the `alertingSaveStateCompressed` feature toggle. @@ -77,7 +91,7 @@ By default, it saves the states every 5 minutes to the database and on each shut can also be configured using the `state_periodic_save_interval` configuration flag. During this process, Grafana deletes all existing alert instances from the database and then writes the entire current set of instances back in batches in a single transaction. Configure the size of each batch using the `state_periodic_save_batch_size` configuration option. -#### Jitter for periodic saves +##### Jitter for batch-based periodic saves To further distribute database load, you can enable jitter for periodic state saves by setting `state_periodic_save_jitter_enabled = true`. When jitter is enabled, instead of saving all batches simultaneously, Grafana spreads the batch writes across a calculated time window of 85% of the save interval. diff --git a/docs/sources/breaking-changes/breaking-changes-v11-0.md b/docs/sources/breaking-changes/breaking-changes-v11-0.md index 313015b275d..898673c4be5 100644 --- a/docs/sources/breaking-changes/breaking-changes-v11-0.md +++ b/docs/sources/breaking-changes/breaking-changes-v11-0.md @@ -57,7 +57,7 @@ In Grafana v11, support for the deprecated AngularJS framework is turned off by #### Migration/mitigation -To avoid disruption, ensure all plugins are up to date and migrate from any remaining AngularJS plugins to a React-based alternative. If a plugin relies on AngularJS, a warning icon and message will be displayed in the [plugins catalog](https://grafana.com/docs/grafana//administration/plugin-management/#plugin-catalog) in Grafana and any dashboard panel where it's used. Additionally, a warning banner will appear in any impacted dashboards. A list of all impacted dashboards can also be generated using the [`detect-angular-dashboards`](https://github.com/grafana/detect-angular-dashboards) tool. +To avoid disruption, ensure all plugins are up to date and migrate from any remaining AngularJS plugins to a React-based alternative. If a plugin relies on AngularJS, a warning icon and message will be displayed in the [plugins catalog](https://grafana.com/docs/grafana//administration/plugin-management/#access-the-plugin-catalog) in Grafana and any dashboard panel where it's used. Additionally, a warning banner will appear in any impacted dashboards. A list of all impacted dashboards can also be generated using the [`detect-angular-dashboards`](https://github.com/grafana/detect-angular-dashboards) tool. Our [documentation](https://grafana.com/docs/grafana//developers/angular_deprecation/angular-plugins/) lists all known public plugins and provides migration advice when possible. diff --git a/docs/sources/datasources/_index.md b/docs/sources/datasources/_index.md index 7ba5e1756a5..e7da61ea2f5 100644 --- a/docs/sources/datasources/_index.md +++ b/docs/sources/datasources/_index.md @@ -71,9 +71,8 @@ After you add and configure a data source, you can use it as an input for many o This documentation describes how to manage data sources in general, and how to configure or query the built-in data sources. -For other data sources, refer to the list of [datasource plugins](/grafana/plugins/). -To develop a custom plugin, refer to [Create a data source plugin](#create-a-data-source-plugin). +For other available plugins, refer to the list of [documented plugins](https://grafana.com/docs/plugins/) or browse the [Plugin catalog](/grafana/plugins/). To develop a custom plugin, refer to [Create a data source plugin](#create-a-data-source-plugin). ## Manage data sources diff --git a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md index 02b229e50f9..9bc7ab64047 100644 --- a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md +++ b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md @@ -250,6 +250,19 @@ You can query CloudWatch Logs using three supported query language options: 1. Select a region. 1. Select **CloudWatch Logs** from the query type drop-down. +1. Select the Logs Mode depending on whether you would like to query CloudWatch Logs Insights or Log Anomalies + +**Log Anomalies** + +[Anomaly detection](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/LogsAnomalyDetection.html) uses machine-learning and pattern recognition to establish baselines of typical log content. +The Log Anomalies query editor fetches the list of anomalies detected in your CloudWatch service. In order to query log anomalies in the editor, a log anomaly detector must be created in the AWS CloudWatch console first. +The log trend cell shows the number of occurrences of the pattern over the selected query time range. +The table shows 50 log anomalies at a time. If you would like to narrow down the list, you can filter anomalies by their ARN and suppressed state. + +In addition to this, you can use the Logs Insights QL editor and the `anomaly` command together with the `patterns` command to define and display log anomalies in real time. See the [CloudWatch Logs Insights](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/LogsAnomalyDetection-Insights.html) documentation for more info. + +**Logs Insights** + 1. Select the query language you would like to use in the **Query Language** drop-down. 1. Click **Select log groups** and choose up to 20 log groups to query. 1. Use the main input area to write your logs query. Amazon CloudWatch only supports a subset of OpenSearch SQL and PPL commands. To find out more about the syntax supported, consult [Amazon CloudWatch Logs documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData_Languages.html) @@ -258,7 +271,7 @@ You can query CloudWatch Logs using three supported query language options: You must specify the region and log groups when querying with **Logs Insights QL** and **OpenSearch PPL**. **OpenSearch SQL** doesn't require log group selection. However, selecting log groups simplifies query writing by populating syntax suggestions with discovered log group fields. {{< /admonition >}} -Click **CloudWatch Logs Insights** to interactively view, search, and analyze your log data in the CloudWatch Logs Insights console. If you're not logged in to the CloudWatch console, the link forwards you to the login page. +Click **View in CloudWatch console** to interactively view, search, and analyze your log data in the CloudWatch Logs Insights console. If you're not logged in to the CloudWatch console, the link forwards you to the login page. ### Query Log groups with OpenSearch SQL diff --git a/docs/sources/developers/http_api/dashboard.md b/docs/sources/developers/http_api/dashboard.md index f5456a22e77..0c886a83b48 100644 --- a/docs/sources/developers/http_api/dashboard.md +++ b/docs/sources/developers/http_api/dashboard.md @@ -634,6 +634,11 @@ Lists all dashboards in the given organization. You can control the maximum numb - namespace: to read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana//developers/http_api/apis/). +**Query parameters**: + +- **`limit`** (optional): Maximum number of dashboards to return +- **`continue`** (optional): Continue token from a previous response to fetch the next page + **Required permissions** See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation. @@ -666,7 +671,7 @@ Content-Length: 644 "apiVersion": "dashboard.grafana.app/v1alpha1", "metadata": { "resourceVersion": "1741315830000", - "continue": "org:1/start:1158/folder:" + "continue": "eyJvIjoxNTIsInYiOjE3NjE3MDQyMjQyMDcxODksInMiOmZhbHNlfQ==" }, "items": [ { @@ -697,6 +702,57 @@ Content-Length: 644 } ``` +The `metadata.continue` field contains a token to fetch the next page. + +**Example subsequent request using continue token**: + +```http +GET /apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards?limit=1&continue=eyJvIjoxNTIsInYiOjE3NjE3MDQyMjQyMDcxODksInMiOmZhbHNlfQ== HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example subsequent response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 + +{ + "kind": "DashboardList", + "apiVersion": "dashboard.grafana.app/v1alpha1", + "items": [ + { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1alpha1", + "metadata": { + "name": "hpqcmg", + "namespace": "default", + "uid": "WQyL7pNTpfGPNlPM6HRJSePrBg5dXmxr4iPQL7txLtwY", + "resourceVersion": "1", + "generation": 1, + "creationTimestamp": "2025-03-06T19:51:31Z", + "annotations": { + "grafana.app/createdBy": "service-account:cef2t2rfm73lsb", + "grafana.app/updatedBy": "service-account:cef2t2rfm73lsb", + "grafana.app/updatedTimestamp": "2025-03-06T19:51:31Z" + } + }, + "spec": { + "schemaVersion": 41, + "title": "Another dashboard", + "uid": "hpqcmg", + "version": 1, + ... + } + } + ] +} +``` + +Continue making requests with the updated `continue` token until you receive a response without a `continue` field in the metadata, indicating you've reached the last page. + Status Codes: - **200** – OK diff --git a/docs/sources/developers/http_api/folder.md b/docs/sources/developers/http_api/folder.md index cd66653e0e6..bc3b6605db0 100644 --- a/docs/sources/developers/http_api/folder.md +++ b/docs/sources/developers/http_api/folder.md @@ -41,6 +41,11 @@ Returns all folders that the authenticated user has permission to view within th - namespace: to read more about the namespace to use, see the [API overview](ref:apis). +**Query parameters**: + +- **`limit`** (optional): Maximum number of folders to return +- **`continue`** (optional): Continue token from a previous response to fetch the next page + **Required permissions** See note in the [introduction]({{< ref "#folder-api" >}}) for an explanation. @@ -67,7 +72,7 @@ Content-Type: application/json "kind": "FolderList", "apiVersion": "folder.grafana.app/v1beta1", "metadata": { - "continue": "org:1/start:1158/folder:" + "continue": "eyJvIjoxNTIsInYiOjE3NjE3MDQyMjQyMDcxODksInMiOmZhbHNlfQ==" }, "items": [ { @@ -93,6 +98,51 @@ Content-Type: application/json } ``` +The `metadata.continue` field contains a token to fetch the next page. + +**Example subsequent request using continue token**: + +```http +GET /apis/folder.grafana.app/v1beta1/namespaces/default/folders?limit=1&continue=eyJvIjoxNTIsInYiOjE3NjE3MDQyMjQyMDcxODksInMiOmZhbHNlfQ== HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example subsequent response**: + +```http +HTTP/1.1 200 +Content-Type: application/json +{ + "kind": "FolderList", + "apiVersion": "folder.grafana.app/v1beta1", + "items": [ + { + "kind": "Folder", + "apiVersion": "folder.grafana.app/v1beta1", + "metadata": { + "name": "bef30vrzxs3y8e", + "namespace": "default", + "uid": "YCtv1FXDsJmTYQoTgcPnfuwZhDZge3uMpXOefaOHjb5Y", + "resourceVersion": "1741343687000", + "creationTimestamp": "2025-03-07T10:35:47Z", + "annotations": { + "grafana.app/createdBy": "service-account:cef2t2rfm73lsb", + "grafana.app/updatedBy": "service-account:cef2t2rfm73lsb", + "grafana.app/updatedTimestamp": "2025-03-07T10:35:47Z" + } + }, + "spec": { + "title": "another folder" + } + } + ] +} +``` + +Continue making requests with the updated `continue` token until you receive a response without a `continue` field in the metadata, indicating you've reached the last page. + Status Codes: - **200** – OK diff --git a/docs/sources/developers/http_api/folder_dashboard_search.md b/docs/sources/developers/http_api/folder_dashboard_search.md index eda90559d99..819a1fcac6c 100644 --- a/docs/sources/developers/http_api/folder_dashboard_search.md +++ b/docs/sources/developers/http_api/folder_dashboard_search.md @@ -36,7 +36,7 @@ Query parameters: - **dashboardUIDs** – List of dashboard uid's to search for - **folderUIDs** – List of folder UIDs to search in - **starred** – Flag indicating if only starred Dashboards should be returned -- **limit** – Limit the number of returned results (max is 5000; default is 1000) +- **limit** – Limit the number of returned results (max is 5000; default is 1000). If an invalid value is provided (for example, strings or special characters), the parameter defaults to 1000. - **page** – Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size. **Example request for retrieving folders and dashboards at the root level**: diff --git a/docs/sources/developers/http_api/query_and_resource_caching.md b/docs/sources/developers/http_api/query_and_resource_caching.md index 83da5e81d1d..1102f593a72 100644 --- a/docs/sources/developers/http_api/query_and_resource_caching.md +++ b/docs/sources/developers/http_api/query_and_resource_caching.md @@ -17,7 +17,6 @@ keywords: labels: products: - enterprise - - oss title: Query and Resource Caching HTTP API --- diff --git a/docs/sources/observability-as-code/_index.md b/docs/sources/observability-as-code/_index.md index 42303dd62f9..33a2c6aeb4d 100644 --- a/docs/sources/observability-as-code/_index.md +++ b/docs/sources/observability-as-code/_index.md @@ -12,6 +12,7 @@ labels: products: - enterprise - oss + - cloud title: Observability as Code weight: 100 cards: @@ -32,11 +33,11 @@ cards: height: 24 href: ./foundation-sdk/ description: The Grafana Foundation SDK is a set of tools, types, and libraries that let you define Grafana dashboards and resources using strongly typed code. - - title: Git Sync (experimental) + - title: Git Sync (private preview) height: 24 href: ./provision-resources/intro-git-sync/ description: Git Sync is an experimental feature that lets you store your dashboard files in a GitHub repository and synchronize those changes with your Grafana instance. - - title: File provisioning (experimental) + - title: File provisioning (private preview) height: 24 href: ./provision-resources/ description: File provisioning in Grafana lets you include resources, including folders and dashboard JSON files, that are stored in a local file system. diff --git a/docs/sources/observability-as-code/grafana-cli/grafanacli-workflows.md b/docs/sources/observability-as-code/grafana-cli/grafanacli-workflows.md index 33489050eed..e0c0d0fe51a 100644 --- a/docs/sources/observability-as-code/grafana-cli/grafanacli-workflows.md +++ b/docs/sources/observability-as-code/grafana-cli/grafanacli-workflows.md @@ -18,7 +18,7 @@ weight: 300 # Manage resources with Grafana CLI {{< admonition type="note" >}} -`grafanactl` is under active development. Command-line flags and subcommands described here may change. This document outlines the target workflows the tool is expected to support. +`grafanactl` is under active development. Command-line flags and subcommands described here may change. This document outlines the target workflows the tool is expected to support. You can find a full list of supported commands [in this page](https://grafana.github.io/grafanactl/reference/cli/grafanactl/). {{< /admonition >}} ## Migrate resources between environments diff --git a/docs/sources/observability-as-code/provision-resources/file-path-setup.md b/docs/sources/observability-as-code/provision-resources/file-path-setup.md index 98fd256e594..6b16926d969 100644 --- a/docs/sources/observability-as-code/provision-resources/file-path-setup.md +++ b/docs/sources/observability-as-code/provision-resources/file-path-setup.md @@ -40,8 +40,7 @@ To set up file sync with local with local files, you need to: Local file provisioning using **Administration** > **Provisioning** will eventually replace the traditional methods Grafana has used for referencing local file systems for dashboard files. {{< admonition type="note" >}} -For production system, we recommend using the `folderFromFilesStructure` capability instead of **Administration** > **Provisioning** to include dashboards from a local file system in your Grafana instance. -Refer to [Provision Grafana](https://grafana.com/docs/grafana//administration/provisioning/#provision-folders-structure-from-filesystem-to-grafana) for more information. +For production systems, use the `folderFromFilesStructure` capability instead of **Administration** > **Provisioning** to include dashboards from a local file system in your Grafana instance. Refer to [Provision Grafana](https://grafana.com/docs/grafana//administration/provisioning/#provision-folders-structure-from-filesystem-to-grafana) for more information. {{< /admonition >}} ### Limitations @@ -125,6 +124,18 @@ The set up process verifies the path and provides an error message if a problem ### Choose what to synchronize +#### Synchronization limitations + +Full instance sync is not available in Grafana Cloud. + +In Grafana OSS/Enterprise: + +- If you try to perform a full instance sync with resources that contain alerts or panels, the connection will be blocked. +- You won't be able to create new alerts or library panels after setup is completed. +- If you opted for full instance sync and want to use alerts and library panels, you'll have to delete the provisioned repository and connect again with folder sync. + +#### Set up synchronization + Choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections). - Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. With this option, all of your dashboards are synced to that one repository. You can only have one provisioned connection with this selection, and you won't have the option of setting up additional repositories to connect to. diff --git a/docs/sources/observability-as-code/provision-resources/git-sync-setup.md b/docs/sources/observability-as-code/provision-resources/git-sync-setup.md index a4bbe8667e7..dd94b7e507b 100644 --- a/docs/sources/observability-as-code/provision-resources/git-sync-setup.md +++ b/docs/sources/observability-as-code/provision-resources/git-sync-setup.md @@ -61,6 +61,8 @@ Refer to [Known limitations](https://grafana.com/docs/grafana// {{< /admonition >}} +### Requirements + To set up Git Sync, you need: - Administration rights in your Grafana organization. @@ -128,34 +130,37 @@ To connect your GitHub repository, follow these steps: ### Choose what to synchronize +In this step you can decide which elements to synchronize. Keep in mind the available options depend on the status of your Grafana instance. + +- If the instance contains resources in an incompatible data format, you'll have to migrate all the data using instance sync. Folder sync won't be supported. +- If there is already another connection using folder sync, instance sync won't be offered. + +#### Synchronization limitations + +Git Sync only supports dashboards and folders. Alerts, panels, and other resources are not supported yet. + {{< admonition type="caution" >}} -Git Sync only works with specific folders for the moment. Full-instance sync is not currently supported. Refer to [Supported resources](/docs/grafana//observability-as-code/provision-resources/intro-git-sync#supported-resources) for more details about which resources you can sync. +Refer to [Known limitations](https://grafana.com/docs/grafana//observability-as-code/provision-resources/intro-git-sync#known-limitations/) before using Git Sync. Refer to [Supported resources](/docs/grafana//observability-as-code/provision-resources/intro-git-sync#supported-resources) for details about which resources you can sync. {{< /admonition >}} -In this step you can decide which elements to synchronize. Keep in mind the available options depend on the status of your GitHub repository. The first time you connect Grafana with a GitHub repository, you need to synchronize with external storage. If you are syncing with a new or empty repository, you won't have an option to migrate dashboards. +Full instance sync is not available in Grafana Cloud. -1. Choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections). +In Grafana OSS/Enterprise: + +- If you try to perform a full instance sync with resources that contain alerts or panels, Git Sync will block the connection. +- You won't be able to create new alerts or library panels after the setup is completed. +- If you opted for full instance sync and want to use alerts and library panels, you'll have to delete the synced repository and connect again with folder sync. + +#### Set up synchronization + +To set up synchronization, choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections). - Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. With this option, all of your dashboards are synced to that one repository. You can only have one provisioned connection with this selection, and you won't have the option of setting up additional repositories to connect to. - - Choose **Sync external storage to new Grafana folder** to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 connections. -1. Enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. -1. Click **Synchronize** to continue. - - +Next, enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. Click **Synchronize** to continue. ### Choose additional settings @@ -163,7 +168,6 @@ Finally, you can set up how often your configured storage is polled for updates. 1. For **Update instance interval (seconds)**, enter how often you want the instance to pull updates from GitHub. The default value is 60 seconds. 1. Optional: Select **Read only** to ensure resources can't be modified in Grafana. - 1. Optional: If you have the Grafana Image Renderer plugin configured, you can **Enable dashboards previews in pull requests**. If image rendering is not available, then you can't select this option. For more information, refer to the [Image Renderer service](https://github.com/grafana/grafana-image-renderer). 1. Select **Finish** to proceed. @@ -179,23 +183,11 @@ You can extend Git Sync by getting instant updates and pull requests using webho ### Set up webhooks for realtime notification and pull request integration -When connecting to a GitHub repository, Git Sync use webhooks to enable real-time updates from GitHub public repositories or enable the pull request integration. -Without webhooks, the polling interval is set in the final configuration screen (default is 60 seconds). -Your Grafana instance must be exposed to the public internet. -You can do this via port forwarding and DNS, a tool such as `ngrok`, or any other method you prefer. +When connecting to a GitHub repository, Git Sync uses webhooks to enable real-time updates from GitHub public repositories or enable pull request integrations. Without webhooks, the polling interval is set in the final configuration screen, and the default is 60 seconds. If you use local storage, then Git Sync only provides periodic pulling. -The permissions set in your GitHub access token provide the authorization for this communication. +You can set up webhooks with whichever service or tooling you prefer. You can use Cloudflare Tunnels with a Cloudflare-managed domain, port-forwarding and DNS options, or a tool such as `ngrok`. -If you use local storage, then Git Sync only provides periodic pulling. - - - -Set up webhooks with whichever service or tooling you prefer. -For example, you can use Cloudflare Tunnels with a Cloudflare-managed domain, port-forwarding and DNS options, or a tool such as `ngrok`. +To set up webhooks you need to expose your Grafana instance to the public Internet. You can do this via port forwarding and DNS, a tool such as `ngrok`, or any other method you prefer. The permissions set in your GitHub access token provide the authorization for this communication. After you have the public URL, you can add it to your Grafana configuration file: @@ -204,23 +196,22 @@ After you have the public URL, you can add it to your Grafana configuration file root_url = https://PUBLIC_DOMAIN.HERE ``` -You can check the configured webhooks in the **View** link for your GitHub repository from **Administration** > **Provisioning**. +To check the configured webhooks, go to **Administration** > **Provisioning** and click the **View** link for your GitHub repository. -#### Necessary paths +#### Expose necessary paths only -If your security setup does not permit publicly exposing the Grafana instance, you can either choose to allowlist the GitHub IP addresses, or expose only the necessary paths. +If your security setup does not permit publicly exposing the Grafana instance, you can either choose to `allowlist` the GitHub IP addresses, or expose only the necessary paths. -The necessary paths required to be exposed are (RegExp): +The necessary paths required to be exposed are, in RegExp: - `/apis/provisioning\.grafana\.app/v0(alpha1)?/namespaces/[^/]+/repositories/[^/]+/(webhook|render/.*)$` ### Set up image rendering for dashboard previews -By setting up image rendering, you can add visual previews of dashboard updates directly in pull requests. -Image rendering also requires webhooks. +Set up image rendering to add visual previews of dashboard updates directly in pull requests. Image rendering also requires webhooks. -You can enable this capability by installing the Grafana Image Renderer in your Grafana instance. For more information and installation instructions, refer to the [Image Renderer service](https://github.com/grafana/grafana-image-renderer). +To enable this capability, install the Grafana Image Renderer in your Grafana instance. For more information and installation instructions, refer to the [Image Renderer service](https://github.com/grafana/grafana-image-renderer). ## Modify configurations after set up is complete diff --git a/docs/sources/observability-as-code/provision-resources/intro-git-sync.md b/docs/sources/observability-as-code/provision-resources/intro-git-sync.md index 25ec1d13806..5b1b1809463 100644 --- a/docs/sources/observability-as-code/provision-resources/intro-git-sync.md +++ b/docs/sources/observability-as-code/provision-resources/intro-git-sync.md @@ -10,6 +10,12 @@ labels: - enterprise - oss - cloud +refs: + roles-and-permissions: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/ title: Git Sync weight: 100 --- @@ -63,14 +69,36 @@ With Git Sync, you can make changes to the files in the provisioned folder in Gi ## Known limitations -Git Sync is under development and the following limitations apply: +{{< admonition type="caution" >}} + +Refer to [Requirements](https://grafana.com/docs/grafana//observability-as-code/provision-resources/git-sync-setup#requirements/) to learn what you need to use Git Sync. + +{{< /admonition >}} + +**Git Sync is under development and the following limitations apply.** + +**Synced resources** - You can only sync dashboards and folders. Refer to [Supported resources](#supported-resources) for more information. - - If you're using Git Sync in Grafana OSS and Grafana Enterprise, some resources might be in an incompatible data format and can't be synced. -- You can only authenticate in GitHub using your Personal Access Token token. -- Support for native Git, Git app, and other providers, such as GitLab or Bitbucket, is on the roadmap. +- If you're using Git Sync in Grafana OSS and Grafana Enterprise, some resources might be in an incompatible data format and won't be synced. +- Full-instance sync is not available in Grafana Cloud and has limitations in Grafana OSS and Grafana Enterprise. Refer to [Choose what to synchronize](../git-sync-setup.md#choose-what-to-synchronize) for more details. +- When migrating to full instance sync, during the synchronization process your resources will be temporarily unavailable. No one will be able to create, edit, or delete resources during this process. +- If you want to manage existing resources with Git Sync, you need to save them as JSON files and commit them to the synced repository. Open a PR to import, copy, move, or save a dashboard. - Restoring resources from the UI is currently not possible. As an alternative, you can restore dashboards directly in your GitHub repository by raising a PR, and they will be updated in Grafana. +**Authentication** + +- You can only authenticate in GitHub using your Personal Access Token token. + +**Permission management** + +- You cannot modify the permissions of a provisioned folder after you've synced it. +- Default permissions are: Admin = Admin, Editor = Editor, and Viewer = Viewer. Refer to [Roles and permissions](ref:roles-and-permissions) for more information. + +**Compatibility** + +- Support for native Git, Git app, and other providers, such as GitLab or Bitbucket, is on the roadmap. + ## Supported resources Git Sync only supports dashboards and folders. Alerts, panels, and other resources are not supported yet. @@ -86,9 +114,9 @@ A resource can be: | **Supported** | The resource can be managed with Git Sync. | The resource is supported but has compatibility issues. It **cannot** be managed with Git Sync. | | **Unsupported** | The resource is **not** supported and **cannot** be managed with Git Sync. | Not applicable. | -### Instance states +### Git Sync instance states -An instance can be in one of the following states: +An instance can be in one of the following Git Sync states: - **Unprovisioned**: None of the instance's resources are being managed by Git Sync. - **Partially provisioned**: Some of the resources are controlled by Git Sync. diff --git a/docs/sources/observability-as-code/schema-v2/variables-schema.md b/docs/sources/observability-as-code/schema-v2/variables-schema.md index 9d6b696eb0f..8706858ea1f 100644 --- a/docs/sources/observability-as-code/schema-v2/variables-schema.md +++ b/docs/sources/observability-as-code/schema-v2/variables-schema.md @@ -29,6 +29,7 @@ The available variable types described in the following sections: - [DatasourceVariableKind](#datasourcevariablekind) - [IntervalVariableKind](#intervalvariablekind) - [CustomVariableKind](#customvariablekind) +- [SwitchVariableKind](#switchvariablekind) - [GroupByVariableKind](#groupbyvariablekind) - [AdhocVariableKind](#adhocvariablekind) @@ -337,6 +338,50 @@ The following table explains the usage of the custom variable JSON fields: | skipUrlSync | bool. Default is `false`. | | description? | string | +## `SwitchVariableKind` + +Following is the JSON for a default switch variable: + +```json + "variables": [ + { + "kind": "SwitchVariable", + "spec": { + "current": "false", + "enabledValue": "true", + "disabledValue": "false", + "hide": "dontHide", + "name": "", + "skipUrlSync": false + } + } + ] +``` + +`SwitchVariableKind` consists of: + +- kind: "SwitchVariable" +- spec: [SwitchVariableSpec](#switchvariablespec) + +### `SwitchVariableSpec` + +The following table explains the usage of the switch variable JSON fields: + + + +| Name | Usage | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| name | string. Name of the variable. | +| current | string. Current value of the switch variable (either `enabledValue` or `disabledValue`). | +| enabledValue | string. Value when the switch is in the enabled state. | +| disabledValue | string. Value when the switch is in the disabled state. | +| label? | string | +| hide | `VariableHide`. Options are: `dontHide`, `hideLabel`, and `hideVariable`. | +| skipUrlSync | bool. Default is `false`. | +| description? | string | + + + ## `GroupByVariableKind` Following is the JSON for a default group by variable: diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index f3d3dd89b45..01b8472f681 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -657,12 +657,16 @@ If you want to track Grafana usage via Azure Application Insights, then specify Optionally, use this option to override the default endpoint address for Application Insights data collecting. For details, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/azure-monitor/app/custom-endpoints?tabs=js). -
+#### `application_insights_auto_route_tracking` + +Optionally, use this to configure `enableAutoRouteTracking` in Azure Application Insights. Defaults to `true`. For more details, refer to the [Azure documentation](https://learn.microsoft.com/en-us/azure/azure-monitor/app/application-insights-faq#is-there-a-way-to-see-fewer-events-per-transaction-when-i-use-the-application-insights-javascript-sdk) #### `feedback_links_enabled` Set to `false` to remove all feedback links from the UI. Default is `true`. +
+ ### `[security]` #### `disable_initial_admin_creation` @@ -1969,6 +1973,12 @@ If a rule frequency is lower than this value, then this value is enforced.
+#### `rule_version_record_limit` + +Defines the limits for how many alert rule versions are stored in the database per alert rule. + +The default `0` value means there's no limit. + ### `[unified_alerting.screenshots]` For more information about screenshots, refer to [Images in notifications](../../alerting/configure-notifications/template-notifications/images-in-notifications/). @@ -2559,7 +2569,7 @@ Available to Grafana administrators only, enables installing, uninstalling, and Set to `true` by default. Setting it to `false` hides the controls. -For more information, refer to [Plugin catalog](../../administration/plugin-management/#plugin-catalog). +For more information, refer to [Plugin catalog](../../administration/plugin-management/#access-the-plugin-catalog). #### `plugin_admin_external_manage_enabled` @@ -2616,6 +2626,15 @@ These will be installed before starting Grafana. Useful when used with provision This option disables all preinstalled plugins. The default is `false`. To disable a specific plugin from being preinstalled, use the `disable_plugins` option. +#### `preinstall_auto_update` + +Enable automatic updates for preinstalled plugins on start-up. +When enabled, preinstalled plugins without a pinned version are automatically updated to the latest version when Grafana starts. + +The default is `true`. + +To prevent automatic updates for specific plugins, pin them to a specific version using the format `plugin_id@version` in the `preinstall` setting. +
### `[live]` 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 d6029c860b2..3dc9644a371 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -34,13 +34,11 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes | | `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | | `unifiedRequestLog` | Writes error logs to the request logger | Yes | -| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | Yes | | `transformationsRedesign` | Enables the transformations redesign | Yes | | `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes | | `dashgpt` | Enable AI powered features in dashboards | Yes | -| `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes | | `formatString` | Enable format string transformer | Yes | | `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | Yes | | `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes | @@ -67,20 +65,19 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | Yes | | `pluginsSriChecks` | Enables SRI checks for plugin assets | | | `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | -| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | | `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes | | `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | | `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes | | `elasticsearchCrossClusterSearch` | Enables cross cluster search in the Elasticsearch data source | | | `lokiLabelNamesQueryApi` | Defaults to using the Loki `/labels` API instead of `/series` | Yes | | `improvedExternalSessionHandlingSAML` | Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly. | Yes | +| `newLogsPanel` | Enables the new logs panel | Yes | | `alertingMigrationUI` | Enables the alerting migration UI, to migrate data source-managed rules to Grafana-managed rules | Yes | | `alertingImportYAMLUI` | Enables a UI feature for importing rules from a Prometheus file to Grafana-managed rules | Yes | | `unifiedNavbars` | Enables unified navbars | | | `grafanaAssistantInProfilesDrilldown` | Enables integration with Grafana Assistant in Profiles Drilldown | Yes | +| `sharingDashboardImage` | Enables image sharing functionality for dashboards | Yes | | `tabularNumbers` | Use fixed-width numbers globally in the UI | | -| `dashboardDsAdHocFiltering` | Enables adhoc filtering support for the dashboard datasource | Yes | -| `adhocFiltersInTooltips` | Enable adhoc filter buttons in visualization tooltips | Yes | | `tempoSearchBackendMigration` | Run search queries through the tempo backend | | ## Public preview feature toggles diff --git a/docs/sources/setup-grafana/set-up-grafana-live.md b/docs/sources/setup-grafana/set-up-grafana-live.md index cfd5725d708..bd490567e71 100644 --- a/docs/sources/setup-grafana/set-up-grafana-live.md +++ b/docs/sources/setup-grafana/set-up-grafana-live.md @@ -55,7 +55,12 @@ The basic streaming example included in Grafana core streams frames with some ge ### Data streaming from Telegraf -A new API endpoint `/api/live/push/:streamId` allows accepting metrics data in Influx format from Telegraf. These metrics are transformed into Grafana data frames and published to channels. +A new API endpoint `/api/live/push/:streamId` allows accepting metrics data in InfluxDB line protocol format from Telegraf. These metrics are transformed into Grafana data frames and published to channels. + +**Important:** When streaming data from Telegraf, ensure that: + +- The data format is set to `influx` in your Telegraf configuration +- Timestamps are in nanoseconds (Unix epoch time) Refer to the tutorial about [streaming metrics from Telegraf to Grafana](/tutorials/stream-metrics-from-telegraf-to-grafana/) for more information. @@ -87,7 +92,44 @@ Channels are lightweight and ephemeral - they are created automatically on user ### Data format -All data travelling over Live channels must be JSON-encoded. +Data sent over Live channels supports multiple formats depending on the endpoint: + +#### WebSocket channels + +Data transmitted through WebSocket channels for real-time streaming to frontend clients must be **JSON-encoded**. This applies to: + +- Dashboard change notifications +- Data source plugin streaming (`ds` scope channels) +- Custom application channels + +#### HTTP Push API + +The `/api/live/push/:streamId` endpoint accepts metrics data in **InfluxDB line protocol format**. This is particularly useful for streaming metrics from tools like Telegraf. + +The line protocol format follows this structure: + +``` +[,=[,=]] =[,=] [] +``` + +**Important:** Timestamps must be encoded in **nanoseconds** (Unix epoch time in nanoseconds). + +**Example:** + +``` +temperature,location=room1,sensor=A temp_celsius=23.5,humidity=45 1651498849000000000 +``` + +In this example: + +- `temperature` is the measurement name +- `location=room1` and `sensor=A` are tags +- `temp_celsius=23.5` and `humidity=45` are fields +- `1651498849000000000` is the timestamp in nanoseconds + +When data is received via the HTTP Push API, it is automatically transformed into Grafana data frames and published to the appropriate channels for visualization. + +For more information about the InfluxDB line protocol format, refer to the [InfluxDB line protocol documentation](https://docs.influxdata.com/influxdb/v1.8/write_protocols/line_protocol_tutorial/). ## Configure Grafana Live diff --git a/docs/sources/shared/visualizations/cell-options.md b/docs/sources/shared/visualizations/cell-options.md index 9521ded500c..a2549edf0ca 100644 --- a/docs/sources/shared/visualizations/cell-options.md +++ b/docs/sources/shared/visualizations/cell-options.md @@ -7,4 +7,5 @@ title: Cell options | ------ | ----------- | | Cell value inspect |

Enables value inspection from table cells. When the switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer which contains two tabs: **Plain text** and **Code editor**.

Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. However, you can switch back and forth between tabs.

| | Tooltip from field | Toggle on the **Tooltip from field** switch to use the values from another field (or column) in a tooltip. For more information, refer to [Tooltip from field](#tooltip-from-field). | +| Styling from field | Toggle on the **Styling from field** switch to apply the styling from another field (or column). The referenced field must contain CSS properties formatted in JSON object syntax (for example, `{"name":"John"}`). For more information, refer to the [Styling from field](#styling-from-field). | diff --git a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md index 6b791739cbb..b1ea811531a 100644 --- a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md +++ b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md @@ -266,7 +266,7 @@ http: You only need this if you don't handle the sub path serving via your reverse proxy configuration. {{< /admonition >}} -If you don't want or can't use the reverse proxy to handle serving Grafana from a _sub path_, you can set the configuration variable `server_from_sub_path` to `true`. +If you don't want or can't use the reverse proxy to handle serving Grafana from a _sub path_, you can set the configuration variable `serve_from_sub_path` to `true`. 1. Include the sub path at the end of the `root_url`. 1. Set `serve_from_sub_path` to `true`: diff --git a/docs/sources/visualizations/dashboards/build-dashboards/annotate-visualizations/index.md b/docs/sources/visualizations/dashboards/build-dashboards/annotate-visualizations/index.md index 47f088783f5..fcccfa6bd1b 100644 --- a/docs/sources/visualizations/dashboards/build-dashboards/annotate-visualizations/index.md +++ b/docs/sources/visualizations/dashboards/build-dashboards/annotate-visualizations/index.md @@ -29,6 +29,16 @@ refs: destination: /docs/grafana//developers/http_api/annotations/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/developer-resources/api-reference/http-api/annotations/ + saved-queries: + - pattern: /docs/grafana/ + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#saved-queries + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#saved-queries + save-query: + - pattern: /docs/grafana/ + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#save-a-query + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#save-a-query --- # Annotate visualizations @@ -148,9 +158,17 @@ To add a new annotation query to a dashboard, follow these steps: {{< figure src="/media/docs/grafana/dashboards/screenshot-annotation-filtering-10-v2.png" max-width="600px" caption="Annotation filtering" >}} -1. Configure the query. +1. To create a query, do one of the following: + - Write or construct a query in the query language of your data source. The annotation query options are different for each data source. For information about annotations in a specific data source, refer to the specific [data source](ref:data-source) topic. + - Click **Replace with saved query** to reuse a [saved query](ref:saved-queries). - The annotation query options are different for each data source. For information about annotations in a specific data source, refer to the specific [data source](ref:data-source) topic. +1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon). +1. (Optional) Click **Test annotation query** to ensure that the query is working properly. +1. (Optional) To add subsequent queries, click **+ Add query** or **+ Add from saved queries**, and test them as many times as needed. + + {{< admonition type="note" >}} + [Saved queries](ref:saved-queries) is currently in [public preview](https://grafana.com/docs/release-life-cycle/) in Grafana Enterprise and Grafana Cloud only. + {{< /admonition >}} 1. Click **Save dashboard**. 1. Click **Back to dashboard** and **Exit edit**. diff --git a/docs/sources/visualizations/dashboards/build-dashboards/create-dashboard/index.md b/docs/sources/visualizations/dashboards/build-dashboards/create-dashboard/index.md index 33ac74c856d..feb6573c3e9 100644 --- a/docs/sources/visualizations/dashboards/build-dashboards/create-dashboard/index.md +++ b/docs/sources/visualizations/dashboards/build-dashboards/create-dashboard/index.md @@ -23,27 +23,27 @@ refs: destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/#special-data-sources visualization-specific-options: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/visualizations/ + destination: /docs/grafana//visualizations/panels-visualizations/visualizations/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/ configure-standard-options: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/configure-standard-options/ + destination: /docs/grafana//visualizations/panels-visualizations/configure-standard-options/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/ configure-value-mappings: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/configure-value-mappings/ + destination: /docs/grafana//visualizations/panels-visualizations/configure-value-mappings/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-value-mappings/ generative-ai-features: - pattern: /docs/grafana/ - destination: /docs/grafana//dashboards/manage-dashboards/#set-up-generative-ai-features-for-dashboards + destination: /docs/grafana//visualizations/dashboards/manage-dashboards/#set-up-generative-ai-features-for-dashboards - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/dashboards/manage-dashboards/#set-up-generative-ai-features-for-dashboards configure-thresholds: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/configure-thresholds/ + destination: /docs/grafana//visualizations/panels-visualizations/configure-thresholds/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-thresholds/ data-sources: @@ -63,27 +63,27 @@ refs: destination: /docs/grafana//administration/roles-and-permissions/ visualizations-options: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/visualizations/ + destination: /docs/grafana//visualizations/panels-visualizations/visualizations/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//panels-visualizations/visualizations/ configure-repeating-panels: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/configure-panel-options/#configure-repeating-panels + destination: /docs/grafana//visualizations/panels-visualizations/configure-panel-options/#configure-repeating-panels - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-panel-options/#configure-repeating-panels override-field-values: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/configure-overrides/ + destination: /docs/grafana//visualizations/panels-visualizations/configure-overrides/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/ saved-queries: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/#saved-queries + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#saved-queries - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#saved-queries save-query: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/#save-a-query + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#save-a-query - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#save-a-query --- @@ -123,18 +123,18 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee For more information about data sources, refer to [Data sources](ref:data-sources) for specific guidelines. -1. To add a query, do one of the following: +1. To create a query, do one of the following: - Write or construct a query in the query language of your data source. - - Click **+ Add from saved queries** to add a previously saved query. - - If you've already written a query, you can click the **Replace with saved query** icon to use a previously saved query instead. + - Click **Replace with saved query** to reuse a [saved query](ref:saved-queries). -1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** icon. +1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon). +1. Click **Refresh** to query the data source. +1. (Optional) To add subsequent queries, click **+ Add query** or **+ Add from saved queries**, and refresh the data source as many times as needed. {{< admonition type="note" >}} - [Saved queries](ref:saved-queries) is in [public preview](https://grafana.com/docs/release-life-cycle/) in Grafana Enterprise and Cloud only. + [Saved queries](ref:saved-queries) is currently in [public preview](https://grafana.com/docs/release-life-cycle/) in Grafana Enterprise and Grafana Cloud only. {{< /admonition >}} -1. Click **Refresh** to query the data source. 1. In the visualization list, select a visualization type. ![Visualization selector](/media/docs/grafana/dashboards/screenshot-select-visualization-11-2.png) diff --git a/docs/sources/visualizations/dashboards/share-dashboards-panels/_index.md b/docs/sources/visualizations/dashboards/share-dashboards-panels/_index.md index 3454f719a0c..a2867759119 100644 --- a/docs/sources/visualizations/dashboards/share-dashboards-panels/_index.md +++ b/docs/sources/visualizations/dashboards/share-dashboards-panels/_index.md @@ -78,6 +78,7 @@ Grafana enables you to share dashboards and panels with other users within your - Snapshots - Embeds - PDFs +- PNG image files - JSON files - Reports - Library panels @@ -98,6 +99,7 @@ You can share dashboards in the following ways: - [As a snapshot](#share-a-snapshot) - [As a PDF export](#export-a-dashboard-as-pdf) - [As a JSON file export](#export-a-dashboard-as-json) +- [As an image export](#export-a-dashboard-as-an-image) When you share a dashboard externally as a link or by email, those dashboards are included in a list of your shared dashboards. To view the list and manage these dashboards, navigate to **Dashboards > Shared dashboards**. @@ -235,6 +237,30 @@ Export a Grafana JSON file that contains everything you need, including layout, 1. Click **Download file** or **Copy to clipboard**. 1. Click the **X** at the top-right corner to close the share drawer. +### Export a dashboard as an image + +{{< admonition type="note">}} +You must have the [Grafana image renderer plugin](https://grafana.com/grafana/plugins/grafana-image-renderer/) installed to export a dashboard as an image. +{{< /admonition >}} + +To export a dashboard in its current state as a PNG image file, follow these steps: + +1. Click **Dashboards** in the main menu. +1. Open the dashboard you want to export. +1. Click the **Export** drop-down list in the top-right corner and select **Export as image**. + + The **Export as image** drawer opens. + +1. Click **Generate image**. + + The image preview is displayed. + +1. Click **Download image**. +1. Click the **X** at the top-right corner to close the share drawer. + +The generated image reflects how the dashboard appears in your browser. +To change it, make changes to the dashboard or browser, like zooming in or out or resizing. + ## Share panels {#share-a-panel} You can share a panels in the following ways: diff --git a/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md b/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md index dc4b41ce1fd..789d6617c18 100644 --- a/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md +++ b/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md @@ -249,6 +249,7 @@ guaranteed because plugin developers can override this functionality. The follow - Dynatrace - Graphite - Google Sheets +- Tempo ### Unconfirmed diff --git a/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md b/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md index 0343d78fb91..69dbf4a2547 100644 --- a/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md +++ b/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md @@ -104,6 +104,7 @@ The following table lists the types of variables shipped with Grafana. | Data source | Quickly change the data source for an entire dashboard. [Add a data source variable](#add-a-data-source-variable). | | Interval | Interval variables represent time spans. [Add an interval variable](#add-an-interval-variable). | | Ad hoc filters | Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). [Add ad hoc filters](#add-ad-hoc-filters). | +| Switch | Display a switch that allows you to toggle between two configurable values for enabled and disabled states. [Add a switch variable](#add-a-switch-variable). | | Global variables | Built-in variables that can be used in expressions in the query editor. Refer to [Global variables](#global-variables). | | Chained variables | Variable queries can contain other variables. Refer to [Chained variables](#chained-variables). | @@ -135,6 +136,7 @@ To create a variable, follow these steps: - [Data source](#add-a-data-source-variable) - [Interval](#add-an-interval-variable) - [Ad hoc filters](#add-ad-hoc-filters) + - [Switch](#add-a-switch-variable) @@ -159,19 +161,10 @@ Query expressions are different for each data source. For more information, refe 1. [Enter general options](#enter-general-options). 1. Under the **Query options** section of the page, select a target data source in the **Data source** drop-down list. - You can also click **Open advanced data source picker** to see more options, including adding a data source (Admins only). For more information about data sources, refer to [Add a data source](ref:add-a-data-source). -1. In the **Query type** drop-down list, select one of the following options: - - **Label names** - - **Label values** - - **Metrics** - - **Query result** - - **Series query** - - **Classic query** - -1. In the **Query** field, enter a query. +1. In the **Query type** drop-down list, select an option and fill in the query fields accordingly. - The query field varies according to your data source. Some data sources have custom query editors. - Each data source defines how the variable values are extracted. The typical implementation uses every string value returned from the data source response as a variable value. Make sure to double-check the documentation for the data source. - Some data sources let you provide custom "display names" for the values. For instance, the PostgreSQL, MySQL, and Microsoft SQL Server plugins handle this by looking for fields named `__text` and `__value` in the result. Other data sources may look for `text` and `value` or use a different approach. Always remember to double-check the documentation for the data source. @@ -183,9 +176,15 @@ Query expressions are different for each data source. For more information, refe - **On dashboard load** - Queries the data source every time the dashboard loads. This slows down dashboard loading, because the variable query needs to be completed before dashboard can be initialized. - **On time range change** - Queries the data source every time the dashboard loads and when the dashboard time range changes. Use this option if your variable options query contains a time range filter or is dependent on the dashboard time range. +1. (Optional) In the **Static options** section of the page, toggle on the **Use static options** switch to add custom options in addition to the query results: + - Make entries in the **Value** and **Display text** fields. + - Click **+ Add new option** to add another static option. + - Repeat these steps as many times as needed. + 1. (Optional) Configure the settings in the [Selection Options](#configure-variable-selection-options) section: - **Multi-value** - Enables multiple values to be selected at the same time. - - **Include All option** - Enables an option to include all variables. + - **Allow custom values** - Enables users to add custom values to the list. + - **Include All option** - Enables an option to include all variables. Enter a value in the **Custom all value** field to set your own "all" option. 1. In the **Preview of values** section, Grafana displays a list of the current variable values. Review them to ensure they match what you expect. 1. Click **Save dashboard**. @@ -384,6 +383,51 @@ If one of the panels in the dashboard using that data source doesn't include tha In cases where the data source you're using doesn't support ad hoc filtering, consider using the special Dashboard data source. For more information, refer to [Filter any data using the Dashboard data source](https://grafana.com/docs/grafana//dashboards/variables/add-template-variables/#filter-any-data-using-the-dashboard-data-source). +## Add a switch variable + +_Switch_ variables display a switch with two configurable values representing enabled and disabled states. This variable type is useful when you need to: + +- Toggle between different query conditions +- Enable or disable specific filters +- Switch between different visualization modes +- Control boolean parameters in your data sources + +1. [Enter general options](#enter-general-options). +1. Under the **Switch options** section of the page, configure the switch values: + + In the **Value pair type** drop-down list, select one of the following predefined options or choose **Custom** to define your own values: + - **True / False** - Uses boolean values `true` and `false` + - **1 / 0** - Uses numeric values `1` and `0` + - **Yes / No** - Uses string values `yes` and `no` + - **Custom** - Allows you to define custom values for both enabled and disabled states + +1. If you selected **Custom** in the previous step, configure the custom values: + - **Enabled value** - Enter the value that represents the enabled state (for example, "on"). + - **Disabled value** - Enter the value that represents the disabled state (for example, "off"). + +1. Click **Save dashboard**. +1. Click **Back to dashboard** and **Exit edit**. + +### Switch variable examples + +The following example shows a switch variable `$debug_mode` used in a Prometheus query to conditionally include debug labels: + +``` +up{job="my-service"} and ($debug_mode == "true" or on() vector(0)) +``` + +The following example shows a switch variable `$show_errors` used to filter log entries: + +``` +{job="application"} |= ($show_errors == "1" ? "ERROR" : "") +``` + +You can also use switch variables in panel titles and other dashboard elements: + +``` +{{#if debug_mode}}Debug Mode: {{/if}}Application Metrics +``` + diff --git a/docs/sources/visualizations/explore/get-started-with-explore.md b/docs/sources/visualizations/explore/get-started-with-explore.md index 8874741cb44..b59ac68c686 100644 --- a/docs/sources/visualizations/explore/get-started-with-explore.md +++ b/docs/sources/visualizations/explore/get-started-with-explore.md @@ -14,12 +14,12 @@ title: Get started with Explore refs: saved-queries: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/#saved-queries + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#saved-queries - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#saved-queries save-query: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/#save-a-query + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#save-a-query - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#save-a-query weight: 5 @@ -71,12 +71,13 @@ Explore consists of a toolbar, outline, query editor, the ability to add multipl - **Run query** - Click to run your query. - **Query editor** - Interface where you construct the query for a specific data source. Query editor elements differ based on data source. In order to run queries across multiple data sources you need to select **Mixed** from the data source picker. - -- **+ Add query** - Add additional queries. -- **+ Add from saved queries** - Add a saved query. If you've already written a query, you can click the **Replace with saved query** icon to use a previously saved query instead. To [save the query](ref:save-query) for reuse, click the **Save query** icon. + - **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon). + - **Replace with saved query** - Reuse a saved query. + - **+ Add query** - Add an additional query. + - **+ Add from saved queries** - Add an additional query by reusing a saved query. {{< admonition type="note" >}} - [Saved queries](ref:saved-queries) is in [public preview](https://grafana.com/docs/release-life-cycle/) in Grafana Enterprise and Cloud only. + [Saved queries](ref:saved-queries) is currently in [public preview](https://grafana.com/docs/release-life-cycle/) in Grafana Enterprise and Grafana Cloud only. {{< /admonition >}} - **Query history** - Query history contains the list of queries that you created in Explore. You can also add queries from the history to your saved queries. Refer to [Query history](/docs/grafana//explore/query-management/#query-history) for detailed information on working with your query history. diff --git a/docs/sources/visualizations/explore/trace-integration.md b/docs/sources/visualizations/explore/trace-integration.md index 9a07f6ad18c..a4edd2a1ec5 100644 --- a/docs/sources/visualizations/explore/trace-integration.md +++ b/docs/sources/visualizations/explore/trace-integration.md @@ -42,7 +42,7 @@ For information on how to use the query editor to create queries for tracing dat ## Trace view -Grafana's trace view provides an overview of a request as it travels through your system. The following sections provide detail on various elements of the trace view. +The trace view provides an overview of a request as it travels through your system. The following sections provide detail on various elements of the trace view. {{< figure src="/media/docs/tempo/screenshot-grafana-trace-view.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view" >}} @@ -76,6 +76,14 @@ Click anywhere on the span row to reveal span details. {{< figure src="/media/docs/tempo/screenshot-grafana-trace-view-timeline.png" class="docs-image--no-shadow" max-width= "900px" caption="Trace view timeline" >}} +The **Span duration bar** highlights the trace’s critical path with a darker segment. This darker segment means the span lies on the trace’s critical path, following the CRISP (Critical Path for Service Performance) algorithm. + +Use the highlight to focus on spans that drive end‑to‑end latency. Spans outside the critical path do not increase the trace duration. + +{{< figure src="/media/docs/grafana/explore/traces-panel-critical-path.png" class="docs-image--no-shadow" max-width= "900px" caption="Critical path span" >}} + +You can also focus on this path by clicking the **Show critical path only** button in the **Span filters** options. + ### Span details Traces are composed of one or more spans. @@ -130,9 +138,6 @@ To only show the spans you have matched, toggle **Show matches only**. Refer to [Span filters](/docs/grafana//datasources/tempo/span-filters/) for more in depth information. -Watch the following video to learn more about filtering trace spans in Grafana: -{{< youtube id="VP2XV3IIc80" >}} - ### Trace to logs You can navigate from a span in a trace view directly to logs relevant for that span. diff --git a/docs/sources/visualizations/panels-visualizations/panel-editor-overview/index.md b/docs/sources/visualizations/panels-visualizations/panel-editor-overview/index.md index 0a2247537cf..4afbd8f71a6 100644 --- a/docs/sources/visualizations/panels-visualizations/panel-editor-overview/index.md +++ b/docs/sources/visualizations/panels-visualizations/panel-editor-overview/index.md @@ -28,7 +28,7 @@ weight: 20 refs: transform-data: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/transform-data/ + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/transform-data/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/transform-data/ the-overview-of-grafana-alerting: @@ -38,22 +38,22 @@ refs: destination: /docs/grafana-cloud/alerting-and-irm/alerting/ table: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/visualizations/table/ + destination: /docs/grafana//visualizations/panels-visualizations/visualizations/table/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/table/ add-a-query: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/#add-a-query + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#add-a-query - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#add-a-query saved-queries: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/#saved-queries + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#saved-queries - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#saved-queries save-query: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/query-transform-data/#save-a-query + destination: /docs/grafana//visualizations/panels-visualizations/query-transform-data/#save-a-query - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#save-a-query --- @@ -88,10 +88,13 @@ The data section contains tabs where you enter queries, transform your data, and - **Queries** - Select your data source. You can also set or update the data source in existing dashboards using the drop-down menu in the **Queries** tab. - - [Add queries](ref:add-a-query). Write or construct a query in the query language of your data source or click **+ Add from saved queries** to add a previously saved query. If you've already written a query, you can click the **Replace with saved query** icon to use a previously saved query instead. To [save the query](ref:save-query) for reuse, click the **Save query** icon. + - **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon). + - **Replace with saved query** - Reuse a saved query. + - **+ Add query** - Add an additional query. + - **+ Add from saved queries** - Add an additional query by reusing a saved query. {{< admonition type="note" >}} - [Saved queries](ref:saved-queries) is in [public preview](https://grafana.com/docs/release-life-cycle/) in Grafana Enterprise and Cloud only. + [Saved queries](ref:saved-queries) is currently in [public preview](https://grafana.com/docs/release-life-cycle/) in Grafana Enterprise and Grafana Cloud only. {{< /admonition >}} - **Transformations** - Apply data transformations. For more information, refer to [Transform data](ref:transform-data). diff --git a/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md b/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md index 42dcf1338b7..b6c9471e3e3 100644 --- a/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md +++ b/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md @@ -129,9 +129,15 @@ You can save queries that you've created so they can be reused by you and others This helps users across your organization create dashboards or find insights in Explore without having to create their own queries or know a query language. It also helps you avoid having several users build the same queries for the same data sources multiple times. +Saved queries are available in: + +- [Dashboards](https://grafana.com/docs/grafana//visualizations/dashboards/build-dashboards/create-dashboard/#create-a-dashboard) +- [Explore](https://grafana.com/docs/grafana//visualizations/explore/get-started-with-explore/#explore-elements) +- [Annotations](https://grafana.com/docs/grafana//visualizations/dashboards/build-dashboards/annotate-visualizations/#add-new-annotation-queries) + You can see a list of these queries in the **Saved queries** drawer: -{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-saved-queries-v12.2png.png" max-width="550px" alt="List of saved queries and the edit query form" caption="The Saved queries drawer accessed from Dashboards" >}} +{{< figure src="/media/docs/grafana/dashboards/screenshot-saved-queries-v12.3.png" max-width="550px" alt="List of saved queries and the edit query form" caption="The **Saved queries** drawer accessed from Dashboards" >}} When you first open the drawer, the list of queries in the **All** tab is filtered by the data source of the panel. However, you can clear that filter to display all saved queries. @@ -150,15 +156,11 @@ In the **Saved queries** drawer, you can: - Edit a query title, description, tags, or the availability of the query to other users in your organization. By default, saved queries are locked for editing. - When you access the **Saved queries** drawer from Explore, you can use the **Edit in Explore** option to edit the body of a query. -Access the duplicate, lock, unlock, and delete query options through the menu in the top-right corner of the query form next to the **Edit** button. +To access your saved queries, click **+ Add from saved queries** or **Replace with saved query** in the query editor: -To access your saved queries, click **+ Add from saved queries** in the query editor: +{{< figure src="/media/docs/grafana/dashboards/screenshot-use-saved-queries-v12.3.png" max-width="750px" alt="Access saved queries" >}} -{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-add-from-saved-2-v12.2.png" max-width="750px" alt="Add a saved query" >}} - -If you've already entered a query, you also have the option to replace it with a saved one: - -{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-replace-w-saved-v12.2.png" max-width="750px" alt="Replace a query with a saved one" >}} +Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace with saved query** updates your existing query. #### Save a query @@ -178,7 +180,6 @@ To save a query you've created: - No validation is performed when you save a query, so it's possible to save an invalid query. You should confirm the query is working properly before you save it. - Saved queries are currently accessible from the query editors in Dashboards and Explore. - You can save a maximum of 1000 queries. -- Users with the Viewer role who have access to Explore can use saved queries, but can't write them. - If you have multiple queries open in Explore and you edit one of them by way of the **Edit in Explore** function in the **Saved queries** drawer, the edited query replaces your open queries in Explore. ### Special data sources @@ -218,12 +219,9 @@ To add a query, follow these steps: For more information about query options, refer to [Query options](#query-options). -1. To add a query, do one of the following: +1. To create a query, do one of the following: - Write or construct a query in the query language of your data source. - - Click **+ Add from saved queries** to add a previously saved query. - - If you've already written a query, you can click the **Replace with saved query** icon to use a previously saved query instead. - -1. (Optional) To save the query for reuse, click the **Save query** icon. + - Click **Replace with saved query** to reuse a saved query. {{< admonition type="note" >}} [Saved queries](#saved-queries) is currently in [public preview](https://grafana.com/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. @@ -231,6 +229,9 @@ To add a query, follow these steps: This feature is only available on Grafana Enterprise and Grafana Cloud. {{< /admonition >}} +1. (Optional) To [save the query](#save-a-query) for reuse, click the **Save query** button (or icon). +1. (Optional) Click **+ Add query** or **Add from saved queries** to add more queries as needed. + 1. Click **Run queries**. Grafana queries the data source and visualizes the data. diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/geomap/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/geomap/index.md index 241622708db..209df209a77 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/geomap/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/geomap/index.md @@ -212,23 +212,20 @@ You can add multiple layers of data to a single geomap in order to create rich, #### Layer type -There are seven map layer types to choose from in a geomap. +There are eight map layer types to choose from in a geomap. - [Markers](#markers-layer) renders a marker at each data point. - [Heatmap](#heatmap-layer) visualizes a heatmap of the data. - [GeoJSON](#geojson-layer) renders static data from a GeoJSON file. - [Night / Day](#night--day-layer) renders a night / day region. -- [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. +- [Route](#route-layer) render data points as a route. +- [Photos](#photos-layer) renders a photo at each data point. +- [Network](#network-layer) 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. -{{< /admonition >}} +- [MapLibre Style layer](#maplibre-style-layer) adds a map from a MapLibre/Mapbox style URL. There are also two experimental (or alpha) layer types. @@ -361,11 +358,7 @@ The Night / Day layer displays night and day regions based on the current time r [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 >}} +#### Route layer The Route layer renders data points as a route. @@ -390,11 +383,7 @@ The layer can also render a route with arrows. [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 >}} +#### Photos layer The Photos layer renders a photo at each data point. @@ -417,11 +406,7 @@ The Photos layer renders a photo at each data point. [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 >}} +#### Network layer 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. @@ -534,6 +519,13 @@ The XYZ Tile layer is a map from a generic tile layer. - [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) +#### MapLibre Style layer + +The MapLibre Style Layer is a map defined using a MapLibre/Mapbox `style.json` URL. The style contains the URL to the tiles, layer definitions, and more. Typically, they're based on vector tiles as opposed to raster tiles. + +- **URL template** - Set a valid style URL. For example: `https://demotiles.maplibre.org/style.json` +- **Access Token** - An API token for mapbox maps. Only works for `mapbox://` URLs. Refer to [mapbox access tokens documentation](https://docs.mapbox.com/help/dive-deeper/access-tokens/) for more information. In other cases, you might have to include the token in the URL. For example: `https://example.com/map/style.json?key=XXX`. + ### 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 @@ -541,12 +533,13 @@ are available each with specific configuration options to style the base map. 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. +There are five 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 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. +- [MapLibre Style layer](#maplibre-style-layer) adds a map from a MapLibre/Mapbox style URL. The default basemap layer uses the CARTO map. You can define custom default base layers in the `.ini` configuration file. @@ -556,7 +549,7 @@ The default basemap layer uses the CARTO map. You can define custom default base You can configure the default base map using config files with Grafana’s provisioning system. For more information on all the settings, refer to the [provisioning docs page](ref:provisioning-docs-page). -Use the JSON configuration option `default_baselayer_config` to define the default base map. There are currently four base map options to choose from: `carto`, `esri-xyz`, `osm-standard`, `xyz`. Here are some provisioning examples for each base map option. +Use the JSON configuration option `default_baselayer_config` to define the default base map. There are currently five base map options to choose from: `carto`, `esri-xyz`, `osm-standard`, `xyz`, `maplibre`. Here are some provisioning examples for each base map option. - **carto** loads the CartoDB tile server. You can choose from `auto`, `dark`, and `light` theme for the base map and can be set as shown below. The `showLabels` tag determines whether or not Grafana shows the Country details on top of the map. Here is an example: @@ -622,6 +615,17 @@ default_baselayer_config = `{ }` ``` +- **maplibre** loads a custom tile server defined by the user. Set a valid style `url` for this option to properly load a default base map. Here's an example: + +```ini +default_baselayer_config = `{ + "type": "maplibre", + "config": { + "url": "https://demotiles.maplibre.org/style.json" + } +}` +``` + `enable_custom_baselayers` allows you to enable or disable custom open source base maps that are already implemented. The default is `true`. ### Map controls options @@ -709,7 +713,7 @@ Displays debug information in the upper right corner. This can be useful for deb #### Tooltip -Tooltips are supported for the **Markers**, **Heatmap**, **Photos** (beta) layers. +Tooltips are supported for the **Markers**, **Heatmap**, **Photos** layers. For these layer types, choose from the following tooltip options: - **None** displays tooltips only when a data point is clicked. diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/logs/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/logs/index.md index 91f179e9979..3af0eeda0d5 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/logs/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/logs/index.md @@ -33,7 +33,20 @@ _Logs_ are structured records of events or messages generated by a system or app The logs visualization displays these records from data sources that support logs, such as Elastic, Influx, and Loki. The logs visualization has colored indicators of log status, as well as collapsible log events that help you analyze the information generated. -![Logs visualization](/media/docs/grafana/panels-visualizations/screenshot-logs-v11.3.png) +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-logs-v12.3.png" max-width="750px" alt="Logs visualization" >}} + +When the log line details are open, you have access to the following additional options: + +- Explain log line in Assistant +- Copy to clipboard +- Anchor to the right +- Close log details + +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-logs-options-v12.3.png" max-width="750px" alt="Log icons" >}} + +You can access the first three options from the log line details or from the log line menu: + +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-log-line-menu-v12.3.png" max-width="300px" alt="The log line menu" >}} {{< docs/play title="Logs Panel" url="https://play.grafana.org/d/6NmftOxZz/" >}} @@ -53,7 +66,7 @@ You can also build log-formatted data from other data sources as long as the fir The second field is used as the log record title regardless of whether it’s a time, numeric, or string field. Usually the second field is a text field containing multiple string elements, but if the message level (or `lvl`) is present, the visualization uses the values in it to add colors to the record, as described in [Log levels integration](ref:log-levels). -Subsequent fields are collapsed inside of each log record and you can open them by clicking the expand (`>`) icon. +Subsequent fields are collapsed inside of each log record. To limit the number of log lines rendered in the visualization, you can use the **Max data points** setting in the panel **Query options**. If that option isn't set, then the data source typically enforces its own default limit. @@ -65,11 +78,11 @@ To limit the number of log lines rendered in the visualization, you can use the | 2023-02-01 11:30:00 | title=Log1 lvl=error | 1 | server2 | 2023-02-01 11:00:00 | | 2023-02-01 11:00:00 | title=Log1 lvl=trace | 1 | server2 | 2023-02-01 11:00:00 | -![Logs Example](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.1-logs-example.png 'Logs Example') +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-logs-example2-v12.3.png" max-width="750px" alt="Logs example" >}} ## Log level -For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions](ref:log-levels). +For logs where a `level` label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions](ref:log-levels). ## Configuration options @@ -85,14 +98,18 @@ Use these settings to refine your visualization: -| Option | Description | -| ------ | ----------- | +| Option | Description | +| --------------- | --------------- | | Time | Show or hide the time column. This is the timestamp associated with the log line as reported from the data source. | | Unique labels | Show or hide the unique labels column, which shows only non-common labels. | | Common labels | Show or hide the common labels. | | Wrap lines | Turn line wrapping on or off. | -| Prettify JSON | Toggle the switch on to pretty print all JSON logs. This setting does not affect logs in any format other than JSON. | +| Enable logs highlighting | Experimental. Use a predefined coloring scheme to highlight relevant parts of the log lines. Subtle colors are added to the log lines to improve readability and help with identifying important information faster. | | Enable log details | Toggle the switch on to see an extendable area with log details including labels and detected fields. Each field or label has a stats icon to display ad-hoc statistics in relation to all displayed logs. The default setting is on. | +| Log details panel mode | Choose to display the log details in a sidebar panel or inline, below the log line. The default mode depends on viewport size: the default mode for smaller viewports is inline, while for larger ones, it's sidebar. You can also change mode dynamically in the panel by clicking the mode control. | +| Enable infinite scrolling | Request more results by scrolling to the bottom of the logs list. When you reach the bottom of the list of logs, if you continue scrolling and the displayed logs are within the selected time interval, you can request to load more logs. When the sort order is **Newest first**, you receive older logs, and when the sort order is **Oldest first** you get newer logs. | +| Show controls | Display controls to jump to the last or first log line, and filter by log level. | +| Font size | Select between the **Default** font size and **Small** font sizes.| | Deduplication | Hide log messages that are duplicates of others shown, according to your selected criteria. Choose from:
  • **Exact** - Ignoring ISO datetimes.
  • **Numerical** - Ignoring only those that differ by numbers such as IPs or latencies.
  • **Signatures** - Removing successive lines with identical punctuation and white space.
| | Order | Set whether to show results **Newest first** or **Oldest first**. | diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/table/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/table/index.md index 84015fa7357..8dea090218f 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/table/index.md @@ -325,9 +325,10 @@ The colored background cell type has the following options: | Apply to entire row | Toggle the switch on to apply the background color that's configured for the cell to the whole row. | | Cell value inspect |

Enables value inspection from table cells. When the switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer which contains two tabs: **Plain text** and **Code editor**.

Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. However, you can switch back and forth between tabs.

| | Tooltip from field | Toggle on the **Tooltip from field** switch to use the values from another field (or column) in a tooltip. For more information, refer to the [Tooltip from field](#tooltip-from-field). | +| Styling from field | Toggle on the **Styling from field** switch to apply the styling from another field (or column). The referenced field must contain CSS properties formatted in JSON object syntax (for example, `{"name":"John"}`). For more information, refer to the [Styling from field](#styling-from-field). | - + #### Data links @@ -346,6 +347,7 @@ The gauge cell type has the following options: | Gauge display mode | Controls the type of gauge used. For more information, refer to the [Gauge display mode](#gauge-display-mode). | | Value display | Controls how the value is displayed. For more information, refer to the [Value display](#value-display). | | Tooltip from field | Toggle on the **Tooltip from field** switch to use the values from another field (or column) in a tooltip. For more information, refer to [Tooltip from field](#tooltip-from-field). | +| Styling from field | Toggle on the **Styling from field** switch to apply the styling from another field (or column). The referenced field must contain CSS properties formatted in JSON object syntax (for example, `{"name":"John"}`). For more information, refer to the [Styling from field](#styling-from-field). | {{< admonition type="note" >}} @@ -402,24 +404,19 @@ For more detailed information about all of the sparkline styling options (except | Point size | Set the size of the points, from 1 to 40 pixels in diameter. | | Bar alignment | Set the position of the bar relative to a data point. | | Tooltip from field | Toggle on the **Tooltip from field** switch to use the values from another field (or column) in a tooltip. For more information, refer to [Tooltip from field](#tooltip-from-field). | +| Styling from field | Toggle on the **Styling from field** switch to apply the styling from another field (or column). The referenced field must contain CSS properties formatted in JSON object syntax (for example, `{"name":"John"}`). For more information, refer to the [Styling from field](#styling-from-field). | #### JSON View This cell type shows values formatted as code. -If a value is an object the JSON view allowing browsing the JSON object will appear on hover. +If a value is an object, the JSON object will appear on hover. {{< figure src="/static/img/docs/tables/json-view.png" max-width="350px" alt="JSON view" class="docs-image--no-shadow" >}} -For the JSON view cell type, you can set enable **Cell value inspect**. -This enables value inspection from table cells. -When the switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer which contains two tabs: **Plain text** and **Code editor**. +It has the following cell options: -Toggle on the **Tooltip from field** switch to use the values from another field (or column) in a tooltip. -For more information, refer to [Tooltip from field](#tooltip-from-field). - -Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. -However, you can switch back and forth between tabs. +{{< docs/shared lookup="visualizations/cell-options.md" source="grafana" version="" >}} #### Pill @@ -510,7 +507,7 @@ Select one of the following options: **Auto**, **Top**, **Right**, **Bottom**, a The content of the tooltip is determined by the values of the source field and can't be directly edited. However, you can affect the display of the value using overrides like value mappings, as shown in the [Example: Tooltip from field with value mappings](#example-tooltip-from-field-with-value-mappings) section. -While you can turn on this option under **Cell options**, and have it applied to all cells in the table, it's typically used as an override on a sub-set of cells instead. +While you can turn on this option under **Cell options** and have it applied to all cells in the table, it's typically used as an override on a sub-set of cells instead. This is demonstrated in the example in the following section. ##### Example: Tooltip from field using overrides @@ -546,6 +543,30 @@ Now, when you hover the cursor over the chip in the "Short text" cell, the mappe You can use all field overrides to affect the display of the tooltip. For example, the **Table > Column width** or **Cell options > Cell type** overrides can change the cell width or visual display of the data. +#### Styling from field + +Toggle on the **Styling from field** switch to apply the styling from another field (or column). +The referenced field must contain [CSS properties](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleProperties) formatted in JSON object syntax. For example: + +```JSON +{"marginLeft":12, "text-decoration": "underline"} +``` + +While you can turn on this option under **Cell options** and have it applied to all cells in the table, it's typically used as an override on a sub-set of cells instead. +This is demonstrated in the following example. + +The following table has six visible fields (columns) as well as a hidden field called "Style": + +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-style-from-field-config-v12.3.png" max-width="750px" alt="Configuration of a table including the styling from field option" >}} + +- The "Style" field has JSON objects with CSS properties. (Note that they are formatted for use in CSV format in this example.) +- The "Style" field is hidden using the **Table > Hide in table** override property. +- The "Info" field is using the **Cell options > Styling from field** override property with the "Style" field as the source. + +The following image shows the "Info" field with the styling from the "Style" field applied: + +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-style-from-field-v12.3.png" max-width="750px" alt="Info field with styling from Style field applied" >}} + ### Standard options {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index bb401ea136d..6779856cdeb 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -170,7 +170,6 @@ aliases: description: Learn about new and updated features in Grafana. labels: products: - - cloud - enterprise - oss menuTitle: What's new @@ -180,7 +179,9 @@ weight: 1 # What's new in Grafana -For release highlights, deprecations, and breaking changes in Grafana releases, refer to these "What's new" pages for each version. +For release highlights, deprecations, and breaking changes in self-managed Grafana releases, refer to these "What's new" pages for each version. + +For information on new Grafana Cloud highlights, refer to [What's new from Grafana Labs](https://grafana.com/whats-new). {{< admonition type="note" >}} For Grafana versions prior to v9.2, additional information might also be available in the archived release notes. To access archived release notes, use the documentation for the minor version you want to see. diff --git a/docs/sources/whatsnew/whats-new-in-v8-0.md b/docs/sources/whatsnew/whats-new-in-v8-0.md index cf3792d8a48..7559f646678 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-0.md +++ b/docs/sources/whatsnew/whats-new-in-v8-0.md @@ -160,7 +160,7 @@ Log navigation in Explore has been significantly improved. We added pagination t You can now use the Plugin catalog app to easily manage your plugins from within Grafana. Install, update, and uninstall plugins without requiring a server restart. -[Plugin catalog](../../administration/plugin-management/#plugin-catalog) was added as a result of this feature. +[Plugin catalog](../../administration/plugin-management/#access-the-plugin-catalog) was added as a result of this feature. ### Performance improvements diff --git a/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts b/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts index 919930027b8..7d8d202cf41 100644 --- a/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts +++ b/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts @@ -9,7 +9,6 @@ import { openScopesSelector, searchScopes, selectScope, - TestScope, } from '../utils/scope-helpers'; import { testScopes } from '../utils/scopes'; diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts index c2bde611cdf..7ef4c67d709 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -18,7 +18,8 @@ test.use({ viewport: { width: 1920, height: 1080 }, }); -test.describe( +// Skipping due to failing test in CI +test.skip( 'Grouping panels', { tag: ['@dashboards'], diff --git a/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts b/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts index 4e008fc3b5f..e47fb1b26f9 100644 --- a/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts +++ b/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts @@ -1,16 +1,9 @@ -import { Page, Locator } from '@playwright/test'; - import { test, expect } from '@grafana/plugin-e2e'; import testDashboard from '../dashboards/AdHocFilterTest.json'; +import { getCell } from '../panels-suite/table-utils'; -// Helper function to get a specific cell in a table -const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: number) => - loc - .getByRole('row') - .nth(rowIdx) - .getByRole(rowIdx === 0 ? 'columnheader' : 'gridcell') - .nth(colIdx); +const fixture = require('../fixtures/prometheus-response.json'); test.describe( 'Dashboard with Table powered by Prometheus data source', @@ -46,80 +39,90 @@ test.describe( gotoDashboardPage, selectors, }) => { - // Handle query and query_range API calls + // Handle query and query_range API calls. Ideally, this would instead be directly tested against gdev-prometheus. await page.route(/\/api\/ds\/query/, async (route) => { - const fixture = require('../fixtures/prometheus-response.json'); - // during the test, we select the "inner_eval" slice to filter; this simulates the behavior - // of prometheus applying that filter and removing dataframes from the response. - if (route.request().postData()?.includes('{slice=\\\"inner_eval\\\"}')) { - delete fixture.results.A.frames[1]; + const response = JSON.parse(JSON.stringify(fixture)); + + // This simulates the behavior of prometheus applying a filter and removing dataframes from the response where + // the label matches the selected filter. We check for either the slice being applied inline into the prometheus + // query or the adhoc filter being present in the request body of prometheus applying that filter and removing + // dataframes from the response. + const postData = route.request().postData(); + const match = + postData?.match(/{slice=\\\"([\w_]+)\\\"}/) ?? + postData?.match(/"adhocFilters":\[{"key":"slice","operator":"equals","value":"([\w_]+)"}\]/); + if (match) { + response.results.A.frames = response.results.A.frames.filter((frame) => + frame.schema.fields.every((field) => !field.labels || field.labels.slice === match[1]) + ); } await route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify(fixture), + body: JSON.stringify(response), }); }); const dashboardPage = await gotoDashboardPage({ uid: dashboardUID }); - const panel = dashboardPage.getByGrafanaSelector( + let panel = dashboardPage.getByGrafanaSelector( selectors.components.Panels.Panel.title('Table powered by Prometheus') ); - await expect(panel).toBeVisible(); + await expect(panel, 'panel is rendered').toBeVisible(); // Wait for the table to load completely - await expect(panel.locator('.rdg')).toBeVisible(); + const table = panel.locator('.rdg'); + await expect(table, 'table is rendered').toBeVisible(); - // Get the first data cell in the third column (row 1, column 2) - const labelValueCell = await getCell(panel, 1, 1); - await expect(labelValueCell).toBeVisible(); + const firstValue = (await getCell(table, 1, 1).textContent())!; + const secondValue = (await getCell(table, 2, 1).textContent())!; + expect(firstValue, `first cell is "${firstValue}"`).toBeTruthy(); + expect(secondValue, `second cell is "${secondValue}"`).toBeTruthy(); + expect(firstValue, 'first and second cell values are different').not.toBe(secondValue); - // Get the cell value before clicking the filter button - const labelValue = await labelValueCell.textContent(); - expect(labelValue).toBeTruthy(); + async function performTest(labelValue: string) { + // Confirm both cells are rendered before we proceed + const otherValue = labelValue === firstValue ? secondValue : firstValue; + await expect(table.getByText(labelValue), `"${labelValue}" is rendered`).toContainText(labelValue); + await expect(table.getByText(otherValue), `"${otherValue}" is rendered`).toContainText(otherValue); - const otherValueCell = await getCell(panel, 2, 1); - const otherValueLabel = await otherValueCell.textContent(); - expect(otherValueLabel).toBeTruthy(); - expect(otherValueLabel).not.toBe(labelValue); + // click the "Filter for value" button on the cell with the specified labelValue + await table.getByText(labelValue).hover(); + table.getByText(labelValue).getByRole('button', { name: 'Filter for value' }).click(); - // Hover over the first cell to trigger the appearance of filter actions - await labelValueCell.hover(); + // Look for submenu items that contain the filtered value + // The adhoc filter should appear as a filter chip or within the variable controls + const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItems.filter({ hasText: labelValue }), `submenu contains "${labelValue}"`).toBeVisible(); + await expect( + submenuItems.filter({ hasText: otherValue }), + `submenu does not contain "${otherValue}"` + ).toBeHidden(); - // Check if the "Filter for value" button appears on hover - const filterForValueButton = labelValueCell.getByRole('button', { name: 'Filter for value' }); - await expect(filterForValueButton).toBeVisible(); + // The URL parameter should contain the filter in format like: var-PromAdHoc=["columnName","=","value"] + const currentUrl = page.url(); + const urlParams = new URLSearchParams(new URL(currentUrl).search); + const promAdHocParam = urlParams.get('var-PromAdHoc'); + expect(promAdHocParam, `url contains "${labelValue}"`).toContain(labelValue); + expect(promAdHocParam, `url does not contain "${otherValue}"`).not.toContain(otherValue); - // Click on the "Filter for value" button - await filterForValueButton.click(); + // finally, let's check that the table was updated and that the value was filtered out when the query was re-run + await expect(table.getByText(labelValue), `"${labelValue}" is still visible`).toHaveText(labelValue); + await expect(table.getByText(otherValue), `"${otherValue}" is filtered out`).toBeHidden(); - // Check if the adhoc filter appears in the dashboard submenu - const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); - await expect(submenuItems.first()).toBeVisible(); + // Remove the adhoc filter by clicking the submenu item again + const filterChip = submenuItems.filter({ hasText: labelValue }); + await filterChip.getByLabel(/Remove filter with key/).click(); + await page.click('body', { position: { x: 0, y: 0 } }); // click outside to close the open menu from ad-hoc filters - // Look for submenu items that contain the filtered value - // The adhoc filter should appear as a filter chip or within the variable controls - const hasFilterValue = await submenuItems.filter({ hasText: labelValue! }).count(); - expect(hasFilterValue).toBeGreaterThan(0); + // the "first" and "second" cells locators don't work here for some reason. + await expect(table.getByText(labelValue), `"${labelValue}" is still rendered`).toContainText(labelValue); + await expect(table.getByText(otherValue), `"${otherValue}" is rendered again`).toContainText(otherValue); + } - const hasOtherValue = await submenuItems.filter({ hasText: otherValueLabel! }).count(); - expect(hasOtherValue).toBe(0); - - // Check if the URL contains the var-PromAdHoc parameter with the filtered value - const currentUrl = page.url(); - expect(currentUrl).toContain('var-PromAdHoc'); - - // The URL parameter should contain the filter in format like: var-PromAdHoc=["columnName","=","value"] - const urlParams = new URLSearchParams(new URL(currentUrl).search); - const promAdHocParam = urlParams.get('var-PromAdHoc'); - expect(promAdHocParam).toBeTruthy(); - expect(promAdHocParam).toContain(labelValue!); - expect(promAdHocParam).not.toContain(otherValueLabel!); - - // finally, let's check that the table was updated and that the value was filtered out when the query was re-run - await expect(otherValueCell).toBeHidden(); + await performTest(firstValue); + await performTest(secondValue); }); } ); diff --git a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts index 1b3f8a326fa..ded0abd61ef 100644 --- a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts @@ -19,9 +19,7 @@ test.describe( // Open dashboard global variables and interpolation await gotoDashboardPage({ uid: DASHBOARD_UID }); - // Get the actual timezone from the browser context (should be Pacific/Easter due to test.use) - const timeZone = await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone); - const example = `Example: from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}`; + const example = `Example: from=now-6h&to=now&timezone=browser`; const expectedItems: string[] = [ '__dashboard = Templating - Global variables and interpolation', @@ -68,10 +66,7 @@ test.describe( // Check link interpolation is working correctly const exampleLink = page.locator(`a:has-text("${example}")`); - await expect(exampleLink).toHaveAttribute( - 'href', - `https://example.com/?from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}` - ); + await expect(exampleLink).toHaveAttribute('href', `https://example.com/?from=now-6h&to=now&timezone=browser`); }); } ); diff --git a/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts b/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts index 271e2f42b1f..6d4b589b79c 100644 --- a/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-time-zone.spec.ts @@ -17,7 +17,7 @@ test.describe( tag: ['@dashboards'], }, () => { - test.fixme('Tests dashboard time zone scenarios', async ({ page, gotoDashboardPage, selectors }) => { + test('Tests dashboard time zone scenarios', async ({ page, gotoDashboardPage, selectors }) => { const dashboardPage = await gotoDashboardPage({ uid: TIMEZONE_DASHBOARD_UID }); const fromTimeZone = 'UTC'; @@ -106,12 +106,18 @@ test.describe( zone: 'Browser', }); - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); + const relativeTimeRow = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) + .locator('[role="row"]') + .filter({ hasText: '00:00:00' }) + .first(); + const timezoneRow = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel in timezone')) + .locator('[role="row"]') + .filter({ hasText: '00:00:00' }) + .first(); + + await expect(relativeTimeRow).toBeVisible(); // Today so far, still in Browser timezone await setTimeRange(page, dashboardPage, selectors, { @@ -119,19 +125,8 @@ test.describe( to: 'now', }); - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); - - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel in timezone')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); + await expect(relativeTimeRow).toBeVisible(); + await expect(timezoneRow).toBeVisible(); // Test UTC timezone await setTimeRange(page, dashboardPage, selectors, { @@ -140,12 +135,7 @@ test.describe( zone: 'Coordinated Universal Time', }); - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); + await expect(relativeTimeRow).toBeVisible(); // Today so far, still in UTC timezone await setTimeRange(page, dashboardPage, selectors, { @@ -153,19 +143,8 @@ test.describe( to: 'now', }); - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); - - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel in timezone')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); + await expect(relativeTimeRow).toBeVisible(); + await expect(timezoneRow).toBeVisible(); // Test Tokyo timezone await setTimeRange(page, dashboardPage, selectors, { @@ -174,12 +153,7 @@ test.describe( zone: 'Asia/Tokyo', }); - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); + await expect(relativeTimeRow).toBeVisible(); // Today so far, still in Tokyo timezone await setTimeRange(page, dashboardPage, selectors, { @@ -187,19 +161,8 @@ test.describe( to: 'now', }); - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); - - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel in timezone')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); + await expect(relativeTimeRow).toBeVisible(); + await expect(timezoneRow).toBeVisible(); // Test LA timezone await setTimeRange(page, dashboardPage, selectors, { @@ -208,12 +171,7 @@ test.describe( zone: 'America/Los Angeles', }); - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); + await expect(relativeTimeRow).toBeVisible(); // Today so far, still in LA timezone await setTimeRange(page, dashboardPage, selectors, { @@ -221,19 +179,8 @@ test.describe( to: 'now', }); - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel with relative time override')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); - - await expect( - dashboardPage - .getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel in timezone')) - .locator('[role="row"]') - .filter({ hasText: '00:00:00' }) - ).toBeVisible(); + await expect(relativeTimeRow).toBeVisible(); + await expect(timezoneRow).toBeVisible(); }); } ); diff --git a/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts index 4cdb7afb081..b9100201f02 100644 --- a/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts +++ b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts @@ -22,133 +22,64 @@ test.describe( tag: ['@panels'], }, () => { - test.describe('Sandbox disabled', () => { - test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); - await page.reload(); + test('Does not add iframes to body', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, }); - test('Add iframes to body', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); + // this button adds 3 iframes to the body + await page.locator('[data-testid="button-create-iframes"]').click(); - // this button adds iframes to the body - await page.locator('[data-testid="button-create-iframes"]').click(); + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; - const iframeIds = [ - 'createElementIframe', - 'innerHTMLIframe', - 'appendIframe', - 'prependIframe', - 'afterIframe', - 'beforeIframe', - 'outerHTMLIframe', - 'parseFromStringIframe', - 'insertBeforeIframe', - 'replaceChildIframe', - ]; - - for (const id of iframeIds) { - await expect(page.locator(`#${id}`)).toBeVisible(); - } - }); - - test('Reaches out of panel div', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); - - // this button reaches out of the panel div and modifies the element dataset - await page.locator('[data-testid="button-reach-out"]').click(); - await expect(page.locator('[data-sandbox-test="true"]')).toBeVisible(); - }); - - test('Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - queryParams: new URLSearchParams({ editPanel: '1' }), - }); - - const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); - await expect(input).toBeEnabled(); - await expect(input).toHaveValue(''); - - await input.fill('x'); - await expect(input).toHaveValue('x'); - await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeVisible(); - }); + for (const id of iframeIds) { + await expect(page.locator(`#${id}`)).toBeHidden(); + } }); - test.describe('Sandbox enabled', () => { - test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); - await page.reload(); + test('Does not reaches out of panel div', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, }); - test('Does not add iframes to body', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); + // this button reaches out of the panel div and modifies the element dataset + await page.locator('[data-testid="button-reach-out"]').click(); + await expect(page.locator('[data-sandbox-test="true"]')).toBeHidden(); + }); - // this button adds 3 iframes to the body - await page.locator('[data-testid="button-create-iframes"]').click(); - - const iframeIds = [ - 'createElementIframe', - 'innerHTMLIframe', - 'appendIframe', - 'prependIframe', - 'afterIframe', - 'beforeIframe', - 'outerHTMLIframe', - 'parseFromStringIframe', - 'insertBeforeIframe', - 'replaceChildIframe', - ]; - - for (const id of iframeIds) { - await expect(page.locator(`#${id}`)).toBeHidden(); - } + test('Does not Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), }); - test('Does not reaches out of panel div', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); + const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); + await expect(input).toBeEnabled(); - // this button reaches out of the panel div and modifies the element dataset - await page.locator('[data-testid="button-reach-out"]').click(); - await expect(page.locator('[data-sandbox-test="true"]')).toBeHidden(); + await input.fill('x'); + await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeHidden(); + }); + + test('Can access specific window global variables', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, }); - test('Does not Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - queryParams: new URLSearchParams({ editPanel: '1' }), - }); - - const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); - await expect(input).toBeEnabled(); - - await input.fill('x'); - await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeHidden(); - }); - - test('Can access specific window global variables', async ({ page, gotoDashboardPage }) => { - await gotoDashboardPage({ - uid: DASHBOARD_ID, - }); - - await page.locator('[data-testid="button-test-globals"]').click(); - await expect(page.locator('[data-sandbox-global="Prism"]')).toBeVisible(); - await expect(page.locator('[data-sandbox-global="jQuery"]')).toBeVisible(); - await expect(page.locator('[data-sandbox-global="location"]')).toBeVisible(); - }); + await page.locator('[data-testid="button-test-globals"]').click(); + await expect(page.locator('[data-sandbox-global="Prism"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="jQuery"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="location"]')).toBeVisible(); }); } ); diff --git a/e2e-playwright/panels-suite/heatmap.spec.ts b/e2e-playwright/panels-suite/heatmap.spec.ts new file mode 100644 index 00000000000..4fe163bf23e --- /dev/null +++ b/e2e-playwright/panels-suite/heatmap.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = '5Y0jv6pVz'; + +// test.use(); + +test.describe('Panels test: Heatmap', { tag: ['@panels', '@heatmap'] }, () => { + test('renders successfully', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + // check that gauges are rendered + const uplot = page.locator('.uplot'); + await expect(uplot, 'panels are rendered').toHaveCount(2); + + // check that no panel errors exist + const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error')); + await expect(errorInfo, 'no errors in the panels').toBeHidden(); + }); + + test('"no data"', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ editPanel: '7' }), + }); + + const uplot = page.locator('.uplot'); + await expect(uplot, "that uplot doesn't appear").toBeHidden(); + + const emptyMessage = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage); + await expect(emptyMessage, 'that the empty text appears').toHaveText('No data'); + }); + + // TODO tooltips, legends, and panel editing +}); diff --git a/e2e-playwright/panels-suite/status-history.spec.ts b/e2e-playwright/panels-suite/status-history.spec.ts new file mode 100644 index 00000000000..e6c7cccac67 --- /dev/null +++ b/e2e-playwright/panels-suite/status-history.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'a2f4ad9e-3b44-4624-8067-35f31be5d309'; + +test.use({ + viewport: { width: 1280, height: 2000 }, +}); + +test.describe('Panels test: StatusHistory', { tag: ['@panels', '@status-history'] }, () => { + test('renders successfully', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + // check that gauges are rendered + const statusHistoryUplot = page.locator('.uplot'); + await expect(statusHistoryUplot, 'panels are rendered').toHaveCount(11); + + // check that no panel errors exist + const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error')); + await expect(errorInfo, 'no errors in the panels').toBeHidden(); + }); + + test('"no data"', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ editPanel: '15' }), + }); + + const statusHistoryUplot = page.locator('.uplot'); + await expect(statusHistoryUplot, "that uplot doesn't appear").toBeHidden(); + + const emptyMessage = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage); + await expect(emptyMessage, 'that the empty text appears').toHaveText('No data'); + + // update the "No value" option and see if the panel updates + const noValueOption = dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Standard options No value')) + .locator('input'); + + await noValueOption.fill('My empty value'); + await noValueOption.blur(); + await expect(emptyMessage, 'that the empty text has changed').toHaveText('My empty value'); + }); + + test('tooltip interactions', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ editPanel: '13' }), + }); + + const statusHistoryUplot = page.locator('.uplot'); + await expect(statusHistoryUplot, 'uplot is rendered').toBeVisible(); + + const tooltip = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Tooltip.Wrapper); + + // hover over a spot to trigger the tooltip + await statusHistoryUplot.hover({ position: { x: 100, y: 50 } }); + await expect(tooltip, 'tooltip appears on hover').toBeVisible(); + await expect(tooltip, 'tooltip displays the value').toContainText('value5'); + + // click to pin the tooltip, hover away to be sure it's pinned + await statusHistoryUplot.click({ position: { x: 100, y: 50 } }); + await statusHistoryUplot.hover({ position: { x: 300, y: 50 } }); + await expect(tooltip, 'tooltip pinned on click').toBeVisible(); + await expect(tooltip, 'tooltip displays the first value').toContainText('value5'); + + // unpin the tooltip, ensure it closes on hover away + await statusHistoryUplot.click({ position: { x: 300, y: 50 } }); + await statusHistoryUplot.blur(); + await expect(tooltip, 'tooltip closed after unpinning and hovering away').toBeHidden(); + + // test clicking the "x" as well + await statusHistoryUplot.click({ position: { x: 100, y: 50 } }); + await expect(tooltip, 'tooltip appears on click').toBeVisible(); + await dashboardPage.getByGrafanaSelector(selectors.components.Portal.container).getByLabel('Close').click(); + await expect(tooltip, 'tooltip closed on "x" click').toBeHidden(); + + // disable tooltips + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Tooltip Tooltip mode')) + .getByLabel('Hidden') + .click(); + await statusHistoryUplot.hover({ position: { x: 100, y: 50 } }); + await expect(tooltip, 'tooltip is not shown when disabled').toBeHidden(); + }); +}); diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts index 7312027d3b6..a14085aa753 100644 --- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts +++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @@ -65,11 +65,11 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100); // test that hover overflow works. - const loremIpsumCell = await getCell(page, 1, longTextColIdx); + const loremIpsumCell = getCell(page, 1, longTextColIdx); await loremIpsumCell.scrollIntoViewIfNeeded(); await loremIpsumCell.hover(); await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100); - await (await getCell(page, 1, longTextColIdx + 1)).hover(); + await getCell(page, 1, longTextColIdx + 1).hover(); await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100); // enable cell inspect, confirm that hover no longer triggers. @@ -140,15 +140,15 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] ).toBeVisible(); // click the "State" column header to sort it. - const stateColumnHeader = await getCell(page, 0, 1); + const stateColumnHeader = getCell(page, 0, 1); await stateColumnHeader.getByText('Info').click(); await expect(stateColumnHeader).toHaveAttribute('aria-sort', 'ascending'); - expect(getCell(page, 1, 1)).resolves.toContainText('down'); // down or down fast + await expect(getCell(page, 1, 1)).toContainText('down'); // down or down fast await stateColumnHeader.getByText('Info').click(); await expect(stateColumnHeader).toHaveAttribute('aria-sort', 'descending'); - expect(getCell(page, 1, 1)).resolves.toContainText('up'); // up or up fast + await expect(getCell(page, 1, 1)).toContainText('up'); // up or up fast await stateColumnHeader.getByText('Info').click(); await expect(stateColumnHeader).not.toHaveAttribute('aria-sort'); @@ -171,7 +171,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] const stateColumnHeader = page.getByRole('columnheader').nth(infoColumnIdx); // get the first value in the "State" column, filter it out, then check that it went away. - const firstStateValue = (await (await getCell(page, 1, infoColumnIdx)).textContent())!; + const firstStateValue = (await getCell(page, 1, infoColumnIdx).textContent())!; await stateColumnHeader.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.HeaderButton).click(); const filterContainer = dashboardPage.getByGrafanaSelector( selectors.components.Panels.Visualization.TableNG.Filters.Container @@ -188,7 +188,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] await expect(filterContainer).not.toBeVisible(); // did it actually filter out our value? - await expect(getCell(page, 1, infoColumnIdx)).resolves.not.toHaveText(firstStateValue); + await expect(getCell(page, 1, infoColumnIdx)).not.toHaveText(firstStateValue); }); test('Tests pagination, row height adjustment', async ({ gotoDashboardPage, selectors, page }) => { @@ -289,7 +289,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] const dataLinkColIdx = await getColumnIdx(page, 'Data Link'); // Info column has a single DataLink by default. - const infoCell = await getCell(page, 1, infoColumnIdx); + const infoCell = getCell(page, 1, infoColumnIdx); await expect(infoCell.locator('a')).toBeVisible(); expect(infoCell.locator('a')).toHaveAttribute('href'); expect(infoCell.locator('a')).not.toHaveAttribute('aria-haspopup'); @@ -306,7 +306,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] continue; } - const cell = await getCell(page, 1, colIdx); + const cell = getCell(page, 1, colIdx); await expect(cell.locator('a')).toBeVisible(); expect(cell.locator('a')).toHaveAttribute('href'); expect(cell.locator('a')).not.toHaveAttribute('aria-haspopup', 'menu'); @@ -319,7 +319,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] // loop thru the columns, click the links, observe that the tooltip appears, and close the tooltip. for (let colIdx = 0; colIdx < colCount; colIdx++) { - const cell = await getCell(page, 1, colIdx); + const cell = getCell(page, 1, colIdx); if (colIdx === infoColumnIdx) { // the Info column should still have its single link. expect(cell.locator('a')).not.toHaveAttribute('aria-haspopup', 'menu'); @@ -433,7 +433,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] await filterContainer.getByTitle('up', { exact: true }).locator('label').click(); await filterContainer.getByRole('button', { name: 'Ok' }).click(); - const cell = await getCell(page, 1, dataLinkColumnIdx); + const cell = getCell(page, 1, dataLinkColumnIdx); await expect(cell).toBeVisible(); await expect(cell).toHaveCSS('text-decoration', /line-through/); diff --git a/e2e-playwright/panels-suite/table-utils.ts b/e2e-playwright/panels-suite/table-utils.ts index 6a225665542..a289c0b211a 100644 --- a/e2e-playwright/panels-suite/table-utils.ts +++ b/e2e-playwright/panels-suite/table-utils.ts @@ -1,6 +1,6 @@ import { Page, Locator } from '@playwright/test'; -export const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: number) => +export const getCell = (loc: Page | Locator, rowIdx: number, colIdx: number) => loc .getByRole('row') .nth(rowIdx) @@ -8,7 +8,7 @@ export const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: numbe .nth(colIdx); export const getCellHeight = async (loc: Page | Locator, rowIdx: number, colIdx: number) => { - const cell = await getCell(loc, rowIdx, colIdx); + const cell = getCell(loc, rowIdx, colIdx); return (await cell.boundingBox())?.height ?? 0; }; @@ -18,7 +18,7 @@ export const getColumnIdx = async (loc: Page | Locator, columnName: string) => { let result = -1; const colCount = await loc.getByRole('columnheader').count(); for (let colIdx = 0; colIdx < colCount; colIdx++) { - const cell = await getCell(loc, 0, colIdx); + const cell = getCell(loc, 0, colIdx); if ((await cell.textContent()) === columnName) { result = colIdx; break; diff --git a/e2e-playwright/panels-suite/timeseries-mouse-zoom-interaction.spec.ts b/e2e-playwright/panels-suite/timeseries-mouse-zoom-interaction.spec.ts new file mode 100644 index 00000000000..c881151f656 --- /dev/null +++ b/e2e-playwright/panels-suite/timeseries-mouse-zoom-interaction.spec.ts @@ -0,0 +1,93 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'TkZXxlNG3'; + +test.describe('Panels test: Timeseries zoom interaction', { tag: ['@panels', '@timeseries'] }, () => { + test('shows zoom cursor during x-axis drag-to-zoom interaction', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + const uplotOverlay = page.locator('.u-over').first(); + await expect(uplotOverlay, 'uplot overlay is visible').toBeVisible(); + + const bbox = await uplotOverlay.boundingBox(); + expect(bbox, 'uplot overlay has dimensions').toBeDefined(); + + await expect(uplotOverlay, 'no zoom cursor initially').not.toHaveClass(/zoom-drag/); + + await page.mouse.move(bbox!.x + 100, bbox!.y + 50); + await page.mouse.down(); + + await expect(uplotOverlay, 'zoom cursor appears on mousedown').toHaveClass(/zoom-drag/); + + await page.mouse.move(bbox!.x + 300, bbox!.y + 50); + + await expect(uplotOverlay, 'zoom cursor persists during drag').toHaveClass(/zoom-drag/); + + await page.mouse.up(); + + await expect(uplotOverlay, 'zoom cursor removed after mouseup').not.toHaveClass(/zoom-drag/); + }); + + test('shows zoom cursor during y-axis drag-to-zoom interaction with Shift key', async ({ + gotoDashboardPage, + page, + }) => { + await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + const uplotOverlay = page.locator('.u-over').first(); + await expect(uplotOverlay, 'uplot overlay is visible').toBeVisible(); + + const bbox = await uplotOverlay.boundingBox(); + expect(bbox, 'uplot overlay has dimensions').toBeDefined(); + + await expect(uplotOverlay, 'no zoom cursor initially').not.toHaveClass(/zoom-drag/); + + await page.keyboard.down('Shift'); + await page.mouse.move(bbox!.x + 100, bbox!.y + 50); + await page.mouse.down(); + + await expect(uplotOverlay, 'zoom cursor appears on mousedown with Shift').toHaveClass(/zoom-drag/); + + await page.mouse.move(bbox!.x + 100, bbox!.y + 150); + + await expect(uplotOverlay, 'zoom cursor persists during drag with Shift').toHaveClass(/zoom-drag/); + + await page.mouse.up(); + await page.keyboard.up('Shift'); + + await expect(uplotOverlay, 'zoom cursor removed after mouseup').not.toHaveClass(/zoom-drag/); + }); + + test('does not show zoom cursor when modifier keys are pressed', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + const uplotOverlay = page.locator('.u-over').first(); + await expect(uplotOverlay, 'uplot overlay is visible').toBeVisible(); + + const bbox = await uplotOverlay.boundingBox(); + + await page.keyboard.down('Control'); + await page.mouse.move(bbox!.x + 100, bbox!.y + 50); + await page.mouse.down(); + + await expect(uplotOverlay, 'no zoom cursor with Ctrl key').not.toHaveClass(/zoom-drag/); + + await page.mouse.up(); + await page.keyboard.up('Control'); + + await page.keyboard.down('Meta'); + await page.mouse.move(bbox!.x + 100, bbox!.y + 50); + await page.mouse.down(); + + await expect(uplotOverlay, 'no zoom cursor with Meta key').not.toHaveClass(/zoom-drag/); + + await page.mouse.up(); + await page.keyboard.up('Meta'); + }); +}); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts index e1fef705a88..0133a3e3712 100644 --- a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts @@ -38,7 +38,7 @@ test.describe( formatExpectError('Could not locate header elements in table panel') ).toContainText(['col1', 'col2']); await expect( - panelEditPage.panel.locator.getByRole('gridcell'), + panelEditPage.panel.data, formatExpectError('Could not locate headers in table panel') ).toContainText(['val1', 'val2', 'val3', 'val4']); }); @@ -58,7 +58,7 @@ test.describe( formatExpectError('Could not locate header elements in table panel') ).toContainText(['col1', 'col2']); await expect( - panelEditPage.panel.locator.getByRole('gridcell'), + panelEditPage.panel.data, formatExpectError('Could not locate data elements in table panel') ).toContainText(['val1', 'val2', 'val3', 'val4']); }); diff --git a/e2e-playwright/smoke-tests-suite/panels.spec.ts b/e2e-playwright/smoke-tests-suite/panels.spec.ts index 822b0ad90b4..08a4189f735 100644 --- a/e2e-playwright/smoke-tests-suite/panels.spec.ts +++ b/e2e-playwright/smoke-tests-suite/panels.spec.ts @@ -7,7 +7,7 @@ test.describe( tag: ['@acceptance'], }, () => { - test('Tests each panel type in the panel edit view to ensure no crash', async ({ + test.skip('Tests each panel type in the panel edit view to ensure no crash', async ({ gotoDashboardPage, selectors, page, diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json index e1960c9515e..0b9f45a25a3 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json @@ -1,6 +1,6 @@ { "name": "@test-plugins/extensions-test-app", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "private": true, "scripts": { "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", @@ -13,7 +13,7 @@ "devDependencies": { "@grafana/plugin-configs": "workspace:*", "@types/lodash": "4.17.7", - "@types/node": "22.15.0", + "@types/node": "24.9.2", "@types/prismjs": "1.26.4", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx index 28775391881..6b284e171bc 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx @@ -10,6 +10,7 @@ export function ExposedComponents() { const { component: ReusableComponent } = usePluginComponent( 'grafana-extensionexample1-app/reusable-component/v1' ); + const { component: AddToDashboardForm } = usePluginComponent('grafana/add-to-dashboard-form/v1'); if (!ReusableComponent) { return null; @@ -20,6 +21,22 @@ export function ExposedComponents() {
+ {AddToDashboardForm && ( +
+

Save to dashboard (exposed form)

+ ({ + type: 'timeseries', + title: 'E2E Add to Dashboard Panel', + targets: [], + })} + // Ensure navigation works correctly from plugin page + options={{ useAbsolutePath: true }} + onClose={() => {}} + /> +
+ )} ); } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json index 90978c2c8a2..57fa87dcde5 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json @@ -80,7 +80,7 @@ "grafanaDependency": ">=10.4.0", "plugins": [], "extensions": { - "exposedComponents": ["grafana-extensionexample1-app/reusable-component/v1"] + "exposedComponents": ["grafana-extensionexample1-app/reusable-component/v1", "grafana/add-to-dashboard-form/v1"] } } } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts index 9f3d3f0bf35..501c929b612 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from '@grafana/plugin-e2e'; import { testIds } from '../testIds'; import pluginJson from '../plugin.json'; +import { ensureExtensionRegistryIsPopulated } from './utils'; test.describe( 'grafana-extensionstest-app', @@ -12,5 +13,34 @@ test.describe( await page.goto(`/a/${pluginJson.id}/exposed-components`); await expect(page.getByTestId(testIds.appB.exposedComponent)).toHaveText('Hello World!'); }); + + test('exposed add-to-dashboard form saves to a new dashboard', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/exposed-components`); + await ensureExtensionRegistryIsPopulated(page); + + // Wait for the exposed form section to be ready + await expect(page.getByRole('heading', { name: 'Save to dashboard (exposed form)' })).toBeVisible(); + + // Wait for any of the form buttons to render (lazy load) before clicking + const openInNewTab = page.getByRole('button', { name: 'Open in new tab' }); + const cancelBtn = page.getByRole('button', { name: 'Cancel' }); + await Promise.race([expect(openInNewTab).toBeVisible(), expect(cancelBtn).toBeVisible()]); + + // Now wait for the submit button to be visible, then click (role or text) + const openDashboardByRole = page.getByRole('button', { name: 'Open dashboard' }); + const openDashboardByText = page.getByText('Open dashboard'); + if (await openDashboardByRole.isVisible().catch(() => false)) { + await openDashboardByRole.click(); + } else { + await expect(openDashboardByText.first()).toBeVisible(); + await openDashboardByText.first().click(); + } + + // Navigates to /dashboard/new and prepopulates a panel from local storage + await expect(page).toHaveURL(/\/dashboard\/new/); + + // Panel should be created with our custom title + await expect(page.getByText('E2E Add to Dashboard Panel').first()).toBeVisible(); + }); } ); diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/package.json b/e2e-playwright/test-plugins/grafana-test-datasource/package.json index 71b032b1d88..8d97d03dc30 100644 --- a/e2e-playwright/test-plugins/grafana-test-datasource/package.json +++ b/e2e-playwright/test-plugins/grafana-test-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@test-plugins/grafana-e2etest-datasource", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "private": true, "scripts": { "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", @@ -13,7 +13,7 @@ "devDependencies": { "@grafana/plugin-configs": "workspace:*", "@types/lodash": "4.17.7", - "@types/node": "22.15.0", + "@types/node": "24.9.2", "@types/prismjs": "1.26.4", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/e2e-playwright/utils/scope-helpers.ts b/e2e-playwright/utils/scope-helpers.ts index 4fb12cc41da..749577ecb05 100644 --- a/e2e-playwright/utils/scope-helpers.ts +++ b/e2e-playwright/utils/scope-helpers.ts @@ -128,7 +128,7 @@ export async function scopeSelectRequest(page: Page, selectedScope: TestScope): export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) { const click = async () => { const element = page.locator( - `[data-testid="scopes-tree-${scopeName}-checkbox"], [data-testid="scopes-tree-${scopeName}-radio"]` + `[data-testid="scopes-tree-${scopeName}-checkbox"], [data-testid="scopes-tree-${scopeName}-radio"], [data-testid="scopes-tree-${scopeName}-link"]` ); await element.scrollIntoViewIfNeeded(); await element.click({ force: true }); @@ -254,9 +254,9 @@ export async function getScopeTreeName(page: Page, nth: number): Promise } export async function getScopeLeafName(page: Page, nth: number): Promise { - const locator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio)/).nth(nth); + const locator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio|link)/).nth(nth); const fullTestId = await locator.getAttribute('data-testid'); - const scopeName = fullTestId?.replace(/^scopes-tree-/, '').replace(/-(checkbox|radio)/, ''); + const scopeName = fullTestId?.replace(/^scopes-tree-/, '').replace(/-(checkbox|radio|link)/, ''); if (!scopeName) { throw new Error('There are no scopes in the selector'); @@ -266,10 +266,11 @@ export async function getScopeLeafName(page: Page, nth: number): Promise } export async function getScopeLeafTitle(page: Page, nth: number): Promise { - const leafLocator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio)/).nth(nth); + // Get the nth selectable tree item (checkbox, radio, or link) + const leafLocator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio|link)/).nth(nth); // Find the closest ancestor element that has the main tree item test id const titleLocator = leafLocator.locator( - 'xpath=ancestor::*[@data-testid][starts-with(@data-testid, "scopes-tree-") and not(contains(@data-testid, "-checkbox")) and not(contains(@data-testid, "-radio")) and not(contains(@data-testid, "-expand"))]' + 'xpath=ancestor::*[@data-testid][starts-with(@data-testid, "scopes-tree-") and not(contains(@data-testid, "-checkbox")) and not(contains(@data-testid, "-radio")) and not(contains(@data-testid, "-link")) and not(contains(@data-testid, "-expand"))]' ); const scopeTitle = await titleLocator.textContent(); if (!scopeTitle) { diff --git a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts index dfa19753b15..15fcdb8016c 100644 --- a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts +++ b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @@ -17,60 +17,24 @@ test.describe( }); test.describe('App Page', () => { - test.describe('Sandbox disabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); - }); + test('Loads the app page with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/a/${APP_ID}`); - test('Loads the app page without the sandbox div wrapper', async ({ page }) => { - await page.goto(`/a/${APP_ID}`); + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); - const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); - await expect(sandboxDiv).toBeHidden(); - - const appPage = page.getByTestId('sandbox-app-test-page-one'); - await expect(appPage).toBeVisible(); - }); - - test('Loads the app configuration without the sandbox div wrapper', async ({ page }) => { - await page.goto(`/plugins/${APP_ID}`); - - const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); - await expect(sandboxDiv).toBeHidden(); - - const configPage = page.getByTestId('sandbox-app-test-config-page'); - await expect(configPage).toBeVisible(); - }); + const appPage = page.getByTestId('sandbox-app-test-page-one'); + await expect(appPage).toBeVisible(); }); - test.describe('Sandbox enabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); - }); + test('Loads the app configuration with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/plugins/${APP_ID}`); - test('Loads the app page with the sandbox div wrapper', async ({ page }) => { - await page.goto(`/a/${APP_ID}`); + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); - const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); - await expect(sandboxDiv).toBeVisible(); - - const appPage = page.getByTestId('sandbox-app-test-page-one'); - await expect(appPage).toBeVisible(); - }); - - test('Loads the app configuration with the sandbox div wrapper', async ({ page }) => { - await page.goto(`/plugins/${APP_ID}`); - - const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); - await expect(sandboxDiv).toBeVisible(); - - const configPage = page.getByTestId('sandbox-app-test-config-page'); - await expect(configPage).toBeVisible(); - }); + const configPage = page.getByTestId('sandbox-app-test-config-page'); + await expect(configPage).toBeVisible(); }); }); } diff --git a/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts index acddc23018c..339d3c24c19 100644 --- a/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts +++ b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts @@ -11,246 +11,127 @@ test.describe( }, () => { test.describe('Config Editor', () => { - test.describe('Sandbox disabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); + test('Should render a sandbox wrapper around the datasource config editor', async ({ + page, + createDataSource, + }) => { + const TIMESTAMP = Date.now(); + const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; + // Add the datasource + const response = await createDataSource({ + type: DATASOURCE_ID, + name: DATASOURCE_TYPED_NAME, }); + const DATASOURCE_CONNECTION_ID = response.uid; + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); - test('Should not render a sandbox wrapper around the datasource config editor', async ({ - page, - createDataSource, - }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); - - const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); - await expect(sandboxDiv).toBeHidden(); - }); + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); }); - test.describe('Sandbox enabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); + test('Should store values in jsonData and secureJsonData correctly', async ({ page, createDataSource }) => { + const TIMESTAMP = Date.now(); + const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; + // Add the datasource + const response = await createDataSource({ + type: DATASOURCE_ID, + name: DATASOURCE_TYPED_NAME, }); + const DATASOURCE_CONNECTION_ID = response.uid; + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); - test('Should render a sandbox wrapper around the datasource config editor', async ({ - page, - createDataSource, - }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + const valueToStore = 'test' + random(100); - const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); - await expect(sandboxDiv).toBeVisible(); - }); + const queryInput = page.locator('[data-testid="sandbox-config-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToStore); + await expect(queryInput).toHaveValue(valueToStore); - test('Should store values in jsonData and secureJsonData correctly', async ({ page, createDataSource }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + const saveButton = page.getByTestId('data-testid Data source settings page Save and Test button'); + await saveButton.click(); - const valueToStore = 'test' + random(100); + const alert = page.getByTestId('data-testid Data source settings page Alert'); + await expect(alert).toBeVisible(); + await expect(alert).toContainText('Sandbox Success'); - const queryInput = page.locator('[data-testid="sandbox-config-editor-query-input"]'); - await expect(queryInput).not.toBeDisabled(); - await queryInput.fill(valueToStore); - await expect(queryInput).toHaveValue(valueToStore); - - const saveButton = page.getByTestId('data-testid Data source settings page Save and Test button'); - await saveButton.click(); - - const alert = page.getByTestId('data-testid Data source settings page Alert'); - await expect(alert).toBeVisible(); - await expect(alert).toContainText('Sandbox Success'); - - // validate the value was stored - await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); - await expect(queryInput).not.toBeDisabled(); - await expect(queryInput).toHaveValue(valueToStore); - }); + // validate the value was stored + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + await expect(queryInput).not.toBeDisabled(); + await expect(queryInput).toHaveValue(valueToStore); }); }); test.describe('Explore Page', () => { - test.describe('Sandbox disabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); - }); + test('Should wrap the query editor in a sandbox wrapper', async ({ + page, + createDataSource, + dashboardPage, + selectors, + }) => { + const TIMESTAMP = Date.now(); + const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; + // Add the datasource + const response = await createDataSource({ + type: DATASOURCE_ID, + name: DATASOURCE_TYPED_NAME, }); + const DATASOURCE_CONNECTION_ID = response.uid; + await page.goto('/explore'); - test('Should not wrap the query editor in a sandbox wrapper', async ({ - page, - createDataSource, - dashboardPage, - selectors, - }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto('/explore'); + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); - const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); - await expect(dataSourcePicker).toBeVisible(); - await dataSourcePicker.click(); + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); - const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); - await expect(datasourceOption).toBeVisible(); - await datasourceOption.scrollIntoViewIfNeeded(); - await datasourceOption.click(); + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); - // make sure the datasource was correctly selected and rendered - const breadcrumb = dashboardPage.getByGrafanaSelector( - selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) - ); - await expect(breadcrumb).toBeVisible(); - - const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); - await expect(sandboxDiv).toBeHidden(); - }); - - test('Should accept values when typed', async ({ page, createDataSource, dashboardPage, selectors }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto('/explore'); - - const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); - await expect(dataSourcePicker).toBeVisible(); - await dataSourcePicker.click(); - - const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); - await expect(datasourceOption).toBeVisible(); - await datasourceOption.scrollIntoViewIfNeeded(); - await datasourceOption.click(); - - // make sure the datasource was correctly selected and rendered - const breadcrumb = dashboardPage.getByGrafanaSelector( - selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) - ); - await expect(breadcrumb).toBeVisible(); - - const valueToType = 'test' + random(100); - - const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); - await expect(queryInput).not.toBeDisabled(); - await queryInput.fill(valueToType); - await expect(queryInput).toHaveValue(valueToType); - }); + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); }); - test.describe('Sandbox enabled', () => { - test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); - }); + test('Should accept values when typed', async ({ page, createDataSource, dashboardPage, selectors }) => { + const TIMESTAMP = Date.now(); + const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; + // Add the datasource + const response = await createDataSource({ + type: DATASOURCE_ID, + name: DATASOURCE_TYPED_NAME, }); + const DATASOURCE_CONNECTION_ID = response.uid; + await page.goto('/explore'); - test('Should wrap the query editor in a sandbox wrapper', async ({ - page, - createDataSource, - dashboardPage, - selectors, - }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto('/explore'); + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); - const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); - await expect(dataSourcePicker).toBeVisible(); - await dataSourcePicker.click(); + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); - const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); - await expect(datasourceOption).toBeVisible(); - await datasourceOption.scrollIntoViewIfNeeded(); - await datasourceOption.click(); + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); - // make sure the datasource was correctly selected and rendered - const breadcrumb = dashboardPage.getByGrafanaSelector( - selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) - ); - await expect(breadcrumb).toBeVisible(); + const valueToType = 'test' + random(100); - const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); - await expect(sandboxDiv).toBeVisible(); - }); + const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToType); + await expect(queryInput).toHaveValue(valueToType); - test('Should accept values when typed', async ({ page, createDataSource, dashboardPage, selectors }) => { - const TIMESTAMP = Date.now(); - const DATASOURCE_TYPED_NAME = `SandboxDatasourceInstance-${TIMESTAMP}`; - // Add the datasource - const response = await createDataSource({ - type: DATASOURCE_ID, - name: DATASOURCE_TYPED_NAME, - }); - const DATASOURCE_CONNECTION_ID = response.uid; - await page.goto('/explore'); - - const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); - await expect(dataSourcePicker).toBeVisible(); - await dataSourcePicker.click(); - - const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); - await expect(datasourceOption).toBeVisible(); - await datasourceOption.scrollIntoViewIfNeeded(); - await datasourceOption.click(); - - // make sure the datasource was correctly selected and rendered - const breadcrumb = dashboardPage.getByGrafanaSelector( - selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) - ); - await expect(breadcrumb).toBeVisible(); - - const valueToType = 'test' + random(100); - - const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); - await expect(queryInput).not.toBeDisabled(); - await queryInput.fill(valueToType); - await expect(queryInput).toHaveValue(valueToType); - - // typing the query editor should reflect in the url - await expect(page).toHaveURL(new RegExp(valueToType)); - }); + // typing the query editor should reflect in the url + await expect(page).toHaveURL(new RegExp(valueToType)); }); }); diff --git a/e2e-playwright/various-suite/grafana-datasource-random-walk.spec.ts b/e2e-playwright/various-suite/grafana-datasource-random-walk.spec.ts new file mode 100644 index 00000000000..15cb4d7bdd4 --- /dev/null +++ b/e2e-playwright/various-suite/grafana-datasource-random-walk.spec.ts @@ -0,0 +1,220 @@ +import { test as base, expect } from '@grafana/plugin-e2e'; + +// Note: Random walk configuration UI requires dashboardTemplates feature toggle to be enabled +const test = base.extend({}); +test.use({ + featureToggles: { + dashboardTemplates: true, + }, +}); + +test.describe( + 'Grafana datasource random walk', + { + tag: ['@grafana-datasource'], + }, + () => { + test('should render random walk configuration fields', async ({ gotoDashboardPage, page, selectors }) => { + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Wait for the first field to be visible (ensures query editor loaded) + await expect(page.locator('#randomWalk-seriesCount-A')).toBeVisible(); + + // Verify core configuration fields (row 1) + await expect(page.locator('#randomWalk-startValue-A')).toBeVisible(); + await expect(page.locator('#randomWalk-min-A')).toBeVisible(); + await expect(page.locator('#randomWalk-max-A')).toBeVisible(); + + // Verify fine-tuning fields (row 2) + await expect(page.locator('#randomWalk-spread-A')).toBeVisible(); + await expect(page.locator('#randomWalk-noise-A')).toBeVisible(); + + // Drop percentage is tested separately in its own test + }); + + test('should configure min and max values and render constrained data', async ({ + gotoDashboardPage, + page, + selectors, + }) => { + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Configure random walk with min/max constraints using specific IDs + const minInput = page.locator('#randomWalk-min-A'); + const maxInput = page.locator('#randomWalk-max-A'); + + await minInput.fill('10'); + await maxInput.fill('50'); + + // Wait for the query to execute + await page.waitForTimeout(1000); + + // Verify graph renders with a series + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + }); + + test('should generate multiple series when series count is configured', async ({ + gotoDashboardPage, + page, + selectors, + }) => { + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Configure series count to 3 using role selector + const seriesCountInput = page.getByRole('spinbutton', { name: 'Series count' }); + await seriesCountInput.fill('3'); + + // Wait for query to execute and check that we have series in the legend + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + + // Verify the input value is set correctly + await expect(seriesCountInput).toHaveValue('3'); + }); + + test('should configure spread and noise parameters', async ({ gotoDashboardPage, page, selectors }) => { + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Configure spread and noise using role selectors + const spreadInput = page.getByRole('spinbutton', { name: 'Spread' }); + const noiseInput = page.getByRole('spinbutton', { name: 'Noise' }); + const startValueInput = page.getByRole('spinbutton', { name: 'Start value' }); + + await startValueInput.fill('100'); + await spreadInput.fill('5'); + await noiseInput.fill('2'); + + // Verify graph renders + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + }); + + test('should configure drop percentage for missing data', async ({ gotoDashboardPage, page, selectors }) => { + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Configure drop percentage using role selector + const dropInput = page.getByRole('spinbutton', { name: 'Drop (%)' }); + await dropInput.fill('20'); + + // Verify graph still renders even with dropped points + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + }); + + test('should show tooltips on configuration fields', async ({ gotoDashboardPage, page }) => { + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Verify the Spread field has a tooltip by hovering near its label + const spreadInput = page.locator('#randomWalk-spread-A'); + await expect(spreadInput).toBeVisible(); + + // The tooltip is part of the InlineField component + // Just verify we can interact with the field (tooltip rendering is handled by the component) + await spreadInput.hover(); + + // Verify the field is configured correctly with tooltip text in the DOM + const spreadLabel = page.getByText('Spread').first(); + await expect(spreadLabel).toBeVisible(); + }); + + test('should maintain configuration values when switching between queries', async ({ + gotoDashboardPage, + page, + selectors, + }) => { + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Configure multiple random walk parameters + await page.locator('#randomWalk-seriesCount-A').fill('2'); + await page.locator('#randomWalk-startValue-A').fill('75'); + await page.locator('#randomWalk-min-A').fill('20'); + await page.locator('#randomWalk-max-A').fill('90'); + await page.locator('#randomWalk-spread-A').fill('3'); + await page.locator('#randomWalk-noise-A').fill('1.5'); + + // Wait for query to execute + await page.waitForTimeout(500); + + // Verify all values are set correctly + await expect(page.locator('#randomWalk-seriesCount-A')).toHaveValue('2'); + await expect(page.locator('#randomWalk-startValue-A')).toHaveValue('75'); + await expect(page.locator('#randomWalk-min-A')).toHaveValue('20'); + await expect(page.locator('#randomWalk-max-A')).toHaveValue('90'); + await expect(page.locator('#randomWalk-spread-A')).toHaveValue('3'); + await expect(page.locator('#randomWalk-noise-A')).toHaveValue('1.5'); + + // Verify graph renders with the configured data + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + }); + + test('should verify series count actually generates multiple series', async ({ + gotoDashboardPage, + page, + selectors, + }) => { + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Set series count to 1 first and verify + await page.locator('#randomWalk-seriesCount-A').fill('1'); + await page.waitForTimeout(500); + + // Check legend - with 1 series we should see only A-series + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + + // Now set series count to 3 + await page.locator('#randomWalk-seriesCount-A').fill('3'); + await page.waitForTimeout(1000); + + // With multiple series, they should all render + // The backend generates multiple frames which should show in the panel + // We can verify by checking the panel has data (legend shows A-series) + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + + // Verify the configuration is set + await expect(page.locator('#randomWalk-seriesCount-A')).toHaveValue('3'); + }); + } +); diff --git a/e2e/cypress/fixtures/long-trace-response-backend.json b/e2e/cypress/fixtures/long-trace-response-backend.json index 5605ab1dd7c..56163b4b3d5 100644 --- a/e2e/cypress/fixtures/long-trace-response-backend.json +++ b/e2e/cypress/fixtures/long-trace-response-backend.json @@ -167,6 +167,16 @@ "3fa414edcef6ad90", "3fa414edcef6ad90", "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90", "3fa414edcef6ad90" ], [ @@ -289,7 +299,17 @@ "0000000000000074", "0000000000000075", "0000000000000076", - "0000000000000077" + "0000000000000077", + "0000000000000078", + "0000000000000079", + "000000000000007a", + "000000000000007b", + "000000000000007c", + "000000000000007d", + "000000000000007e", + "000000000000007f", + "0000000000000080", + "0000000000000081" ], [ "", @@ -411,7 +431,17 @@ "0000000000000039", "000000000000003a", "000000000000003a", - "000000000000003b" + "000000000000003b", + "000000000000003b", + "0000000000000078", + "0000000000000078", + "0000000000000079", + "0000000000000079", + "000000000000007a", + "000000000000007a", + "000000000000007b", + "000000000000007b", + "000000000000007c" ], [ "GET /api/health", @@ -533,7 +563,17 @@ "GET /rollback/prepare", "POST /commit/save", "GET /branch/merge", - "POST /tag/create" + "POST /tag/create", + "GET /release/notes", + "POST /changelog/generate", + "GET /documentation/build", + "POST /test/run", + "GET /coverage/report", + "POST /artifact/publish", + "GET /registry/pull", + "POST /image/push", + "GET /manifest/inspect", + "POST /deployment/verify" ], [ "api-gateway", @@ -655,7 +695,17 @@ "rollback-mgr", "git-api", "merge-api", - "tag-api" + "tag-api", + "release-api", + "changelog-gen", + "doc-builder", + "test-runner", + "coverage-tool", + "artifact-mgr", + "registry-client", + "image-pusher", + "manifest-parser", + "deploy-verifier" ], [ 1579270400000, 1579270401000, 1579270402000, 1579270403000, 1579270404000, 1579270405000, 1579270406000, @@ -675,7 +725,8 @@ 1579270498000, 1579270499000, 1579270500000, 1579270501000, 1579270502000, 1579270503000, 1579270504000, 1579270505000, 1579270506000, 1579270507000, 1579270508000, 1579270509000, 1579270510000, 1579270511000, 1579270512000, 1579270513000, 1579270514000, 1579270515000, 1579270516000, 1579270517000, 1579270518000, - 1579270519000 + 1579270519000, 1579270520000, 1579270521000, 1579270522000, 1579270523000, 1579270524000, 1579270525000, + 1579270526000, 1579270527000, 1579270528000, 1579270529000 ], [ 100000, 50000, 75000, 200000, 25000, 150000, 80000, 300000, 45000, 120000, 60000, 180000, 35000, 250000, @@ -686,7 +737,8 @@ 190000, 58000, 145000, 92000, 220000, 48000, 175000, 85000, 205000, 65000, 180000, 72000, 195000, 55000, 160000, 98000, 225000, 62000, 140000, 78000, 200000, 48000, 165000, 85000, 215000, 58000, 150000, 88000, 230000, 45000, 175000, 72000, 185000, 62000, 155000, 95000, 210000, 52000, 170000, 82000, 195000, 68000, - 145000, 92000, 235000, 58000, 160000, 75000, 180000, 65000 + 145000, 92000, 235000, 58000, 160000, 75000, 180000, 65000, 125000, 78000, 190000, 52000, 165000, 88000, + 215000, 62000, 155000, 95000 ] ] } diff --git a/e2e/old-arch/various-suite/trace-view-scrolling.spec.ts b/e2e/old-arch/various-suite/trace-view-scrolling.spec.ts index f73c3c6f2c1..372c2d11032 100644 --- a/e2e/old-arch/various-suite/trace-view-scrolling.spec.ts +++ b/e2e/old-arch/various-suite/trace-view-scrolling.spec.ts @@ -33,15 +33,15 @@ describe('Trace view', () => { e2e.components.TraceViewer.spanBar().should('be.visible'); + e2e.components.TraceViewer.spanBar().its('length').should('be.equal', 100); + + e2e.pages.Explore.General.scrollView().children().first().scrollTo('bottom'); + + // After scrolling we should see 50 spans e2e.components.TraceViewer.spanBar() .its('length') - .then((oldLength) => { - e2e.pages.Explore.General.scrollView().children().first().scrollTo('center'); - - // After scrolling we should load more spans - e2e.components.TraceViewer.spanBar().should(($span) => { - expect($span.length).to.be.gt(oldLength); - }); + .should(($span) => { + expect($span).to.be.equal(50); }); }); }); diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 7016dcf8169..57b56b49744 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -24,11 +24,6 @@ "count": 1 } }, - "packages/grafana-data/src/dataframe/ArrayDataFrame.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "packages/grafana-data/src/dataframe/CircularDataFrame.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -36,7 +31,7 @@ }, "packages/grafana-data/src/dataframe/DataFrameView.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 2 + "count": 1 }, "@typescript-eslint/no-explicit-any": { "count": 2 @@ -44,28 +39,23 @@ }, "packages/grafana-data/src/dataframe/MutableDataFrame.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 3 + "count": 1 }, "@typescript-eslint/no-explicit-any": { - "count": 9 + "count": 6 } }, "packages/grafana-data/src/dataframe/StreamingDataFrame.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 4 - } - }, - "packages/grafana-data/src/dataframe/processDataFrame.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 + "count": 3 } }, "packages/grafana-data/src/dataframe/processDataFrame.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 15 + "count": 11 }, "@typescript-eslint/no-explicit-any": { - "count": 5 + "count": 4 } }, "packages/grafana-data/src/datetime/moment_wrapper.ts": { @@ -106,7 +96,7 @@ "count": 1 }, "@typescript-eslint/no-explicit-any": { - "count": 5 + "count": 4 } }, "packages/grafana-data/src/panel/registryFactories.ts": { @@ -305,7 +295,7 @@ }, "packages/grafana-data/src/utils/csv.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 3 + "count": 2 }, "@typescript-eslint/no-explicit-any": { "count": 2 @@ -347,11 +337,6 @@ "count": 3 } }, - "packages/grafana-data/src/vector/CircularVector.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "packages/grafana-data/src/vector/FunctionalVector.ts": { "@typescript-eslint/no-explicit-any": { "count": 9 @@ -539,11 +524,6 @@ "count": 2 } }, - "packages/grafana-sql/src/components/ErrorBoundary.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx": { "no-restricted-syntax": { "count": 2 @@ -602,14 +582,6 @@ "count": 5 } }, - "packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx": { - "no-restricted-syntax": { - "count": 1 - }, - "react-hooks/rules-of-hooks": { - "count": 5 - } - }, "packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -630,32 +602,14 @@ "count": 1 } }, - "packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx": { - "no-restricted-syntax": { - "count": 3 - } - }, "packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 }, - "no-restricted-syntax": { - "count": 3 - }, "react-prefer-function-component/react-prefer-function-component": { "count": 1 } }, - "packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx": { - "no-restricted-syntax": { - "count": 3 - } - }, - "packages/grafana-ui/src/components/DataSourceSettings/TLSAuthSettings.tsx": { - "no-restricted-syntax": { - "count": 3 - } - }, "packages/grafana-ui/src/components/DataSourceSettings/types.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 @@ -666,16 +620,6 @@ "count": 3 } }, - "packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/grafana-ui/src/components/FormField/FormField.tsx": { "no-restricted-syntax": { "count": 1 @@ -792,7 +736,7 @@ }, "packages/grafana-ui/src/components/Modal/ModalsContext.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 4 + "count": 2 }, "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -808,11 +752,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/grafana-ui/src/components/PanelChrome/PanelContext.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 @@ -870,7 +809,7 @@ }, "packages/grafana-ui/src/components/Select/SelectBase.tsx": { "@typescript-eslint/consistent-type-assertions": { - "count": 2 + "count": 1 }, "@typescript-eslint/no-explicit-any": { "count": 4 @@ -883,7 +822,7 @@ }, "packages/grafana-ui/src/components/Select/resetSelectStyles.ts": { "@typescript-eslint/no-explicit-any": { - "count": 4 + "count": 1 } }, "packages/grafana-ui/src/components/Select/types.ts": { @@ -898,7 +837,7 @@ }, "packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 3 + "count": 2 }, "@typescript-eslint/no-explicit-any": { "count": 13 @@ -932,11 +871,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/Table/TableCellInspector.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 @@ -1288,21 +1222,11 @@ "count": 1 } }, - "public/app/core/components/Select/ServiceAccountPicker.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/components/Select/TeamPicker.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 } }, - "public/app/core/components/Select/UserPicker.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/components/SharedPreferences/SharedPreferences.tsx": { "no-restricted-syntax": { "count": 8 @@ -1352,11 +1276,6 @@ "count": 2 } }, - "public/app/core/components/TimelineChart/TimelineChart.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/config.ts": { "no-barrel-files/no-barrel-files": { "count": 2 @@ -1493,25 +1412,17 @@ "public/app/features/admin/UserLdapSyncInfo.tsx": { "no-restricted-syntax": { "count": 3 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/admin/UserOrgs.tsx": { "no-restricted-syntax": { "count": 2 }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 3 - } - }, - "public/app/features/admin/UserProfile.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 } }, - "public/app/features/admin/UserSessions.tsx": { + "public/app/features/admin/UserProfile.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 } @@ -2062,21 +1973,6 @@ "count": 2 } }, - "public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingData.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingTimeRangeSize.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "public/app/features/dashboard-scene/conditional-rendering/conditions/ConditionalRenderingVariable.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -2115,11 +2011,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -2135,11 +2026,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx": { - "no-restricted-syntax": { - "count": 4 - } - }, "public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx": { "no-restricted-syntax": { "count": 1 @@ -2364,10 +2250,10 @@ }, "public/app/features/dashboard/api/ResponseTransformers.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 4 + "count": 2 }, "@typescript-eslint/no-explicit-any": { - "count": 3 + "count": 1 } }, "public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx": { @@ -2429,9 +2315,6 @@ "public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx": { "no-restricted-syntax": { "count": 5 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx": { @@ -2782,11 +2665,6 @@ "count": 1 } }, - "public/app/features/datasources/components/DataSourcePluginSettings.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/datasources/components/DataSourcePluginState.tsx": { "no-restricted-syntax": { "count": 3 @@ -2902,11 +2780,6 @@ "count": 1 } }, - "public/app/features/explore/ExploreTimeControls.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/explore/Logs/LiveLogs.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -2962,31 +2835,11 @@ "count": 1 } }, - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx": { "react-hooks/rules-of-hooks": { "count": 1 } }, - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -3007,11 +2860,6 @@ "count": 1 } }, - "public/app/features/explore/TraceView/components/common/SearchBarInput.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/explore/TraceView/components/demo/trace-generators.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -3148,11 +2996,6 @@ "count": 2 } }, - "public/app/features/live/LiveConnectionWarning.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/live/centrifuge/LiveDataStream.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -3183,14 +3026,6 @@ "count": 1 } }, - "public/app/features/logs/utils.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - }, - "no-restricted-syntax": { - "count": 6 - } - }, "public/app/features/manage-dashboards/DashboardImportPage.tsx": { "no-restricted-syntax": { "count": 2 @@ -3407,16 +3242,6 @@ "count": 3 } }, - "public/app/features/provisioning/Config/ConfigFormGithubCollapse.tsx": { - "no-restricted-syntax": { - "count": 2 - } - }, - "public/app/features/provisioning/File/FileStatusPage.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 2 - } - }, "public/app/features/provisioning/Shared/BranchValidationError.tsx": { "react/no-unescaped-entities": { "count": 26 @@ -3621,11 +3446,6 @@ "count": 1 } }, - "public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -3743,11 +3563,6 @@ "count": 2 } }, - "public/app/features/variables/constant/ConstantVariableEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/variables/constant/reducer.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -3830,9 +3645,6 @@ "public/app/features/variables/pickers/shared/VariableInput.tsx": { "no-restricted-syntax": { "count": 1 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/variables/pickers/shared/VariableOptions.tsx": { @@ -4047,11 +3859,6 @@ "count": 1 } }, - "public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx": { "no-restricted-syntax": { "count": 1 @@ -4250,11 +4057,6 @@ "count": 1 } }, - "public/app/plugins/datasource/grafana-testdata-datasource/components/CSVWaveEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 2 @@ -4873,14 +4675,6 @@ "count": 1 } }, - "public/app/plugins/panel/heatmap/HeatmapPanel.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - }, - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "public/app/plugins/panel/heatmap/migrations.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -4904,7 +4698,7 @@ }, "public/app/plugins/panel/heatmap/utils.ts": { "@typescript-eslint/consistent-type-assertions": { - "count": 17 + "count": 16 } }, "public/app/plugins/panel/histogram/Histogram.tsx": { @@ -4914,7 +4708,7 @@ }, "public/app/plugins/panel/live/LiveChannelEditor.tsx": { "@typescript-eslint/consistent-type-assertions": { - "count": 2 + "count": 1 }, "@typescript-eslint/no-explicit-any": { "count": 1 @@ -4963,11 +4757,6 @@ "count": 1 } }, - "public/app/plugins/panel/stat/StatPanel.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/panel/state-timeline/migrations.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 diff --git a/eslint.config.js b/eslint.config.js index f01463559aa..2af812e1fe3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -434,6 +434,7 @@ module.exports = [ 'public/app/plugins/datasource/loki/**/*.{ts,tsx}', 'public/app/plugins/datasource/loki/**/*.{ts,tsx}', 'public/app/plugins/datasource/mysql/**/*.{ts,tsx}', + 'public/app/plugins/datasource/opentsdb/**/*.{ts,tsx}', 'public/app/plugins/datasource/parca/**/*.{ts,tsx}', 'public/app/plugins/datasource/tempo/**/*.{ts,tsx}', 'public/app/plugins/datasource/zipkin/**/*.{ts,tsx}', @@ -485,6 +486,25 @@ module.exports = [ }, }, + // dataviz prefers to use `clsx` over `cx` to compose classes as a rule for performance reasons + { + files: ['public/app/plugins/panel/state-timeline/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 'error', + withBaseRestrictedImportsConfig({ + patterns: [ + { + group: ['@emotion/css'], + importNames: ['cx'], + message: 'Do not use "cx" from @emotion/css. Instead, use `clsx` and compose together only strings.', + }, + ], + }), + ], + }, + }, + // Old betterer rules config: { files: ['**/*.{js,jsx,ts,tsx}'], @@ -580,4 +600,17 @@ module.exports = [ ], }, }, + + // { + // name: 'grafana/plugin-external-import-paths', + // files: [ + // 'public/app/plugins/panel/histogram/**/*.{ts,tsx}', + // ], + // plugins: { + // '@grafana': grafanaPlugin, + // }, + // rules: { + // '@grafana/no-plugin-external-import-paths': 'error', + // }, + // }, ]; diff --git a/go.mod b/go.mod index 70244a7d8af..873633a0dad 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/apache/arrow-go/v18 v18.4.1 // @grafana/plugins-platform-backend github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources - github.com/aws/aws-sdk-go-v2 v1.38.1 // @grafana/aws-datasources + github.com/aws/aws-sdk-go-v2 v1.39.1 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources @@ -53,7 +53,7 @@ require ( github.com/dgraph-io/badger/v4 v4.7.0 // @grafana/grafana-search-and-storage github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e // @grafana/grafana-datasources-core-services - github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 // @grafana/grafana-datasources-core-services + github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33 // @grafana/grafana-datasources-core-services github.com/dustin/go-humanize v1.0.1 // @grafana/observability-traces-and-profiling github.com/emicklei/go-restful/v3 v3.13.0 // @grafana/grafana-app-platform-squad github.com/fatih/color v1.18.0 // @grafana/grafana-backend-group @@ -63,7 +63,7 @@ require ( github.com/go-jose/go-jose/v4 v4.1.2 // @grafana/identity-access-team github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group github.com/go-ldap/ldap/v3 v3.4.4 // @grafana/identity-access-team - github.com/go-logfmt/logfmt v0.6.0 // @grafana/oss-big-tent + github.com/go-logfmt/logfmt v0.6.1 // @grafana/oss-big-tent github.com/go-openapi/loads v0.23.1 // @grafana/alerting-backend github.com/go-openapi/runtime v0.28.0 // @grafana/alerting-backend github.com/go-openapi/strfmt v0.24.0 // @grafana/alerting-backend @@ -83,7 +83,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.7.0 // @grafana/grafana-backend-group - github.com/googleapis/gax-go/v2 v2.14.2 // @grafana/grafana-backend-group + github.com/googleapis/gax-go/v2 v2.15.0 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae // @grafana/alerting-backend @@ -96,20 +96,20 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.47.0 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/logging v0.46.0 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-aws-sdk v1.2.0 // @grafana/aws-datasources + github.com/grafana/grafana-app-sdk v0.48.1 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk/logging v0.48.1 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-aws-sdk v1.3.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.4.2 // @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.281.0 // @grafana/plugins-platform-backend + github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 // @grafana/alerting-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs - github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 // indirect; @grafana/grafana-git-ui-sync-team + github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b // indirect; @grafana/grafana-git-ui-sync-team github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling - github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-search-and-storage github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // @grafana/grafana-backend-group @@ -128,7 +128,7 @@ require ( github.com/jackc/pgx/v5 v5.7.6 // @grafana/grafana-search-and-storage github.com/jmespath-community/go-jmespath v1.1.1 // @grafana/identity-access-team github.com/jmespath/go-jmespath v0.4.0 // indirect; // @grafana/grafana-backend-group - github.com/jmoiron/sqlx v1.3.5 // @grafana/grafana-backend-group + github.com/jmoiron/sqlx v1.4.0 // @grafana/grafana-backend-group github.com/json-iterator/go v1.1.12 // @grafana/grafana-backend-group github.com/lib/pq v1.10.9 // @grafana/grafana-backend-group github.com/m3db/prometheus_remote_client_golang v0.4.4 // @grafana/grafana-backend-group @@ -170,18 +170,18 @@ require ( github.com/spf13/pflag v1.0.10 // @grafana-app-platform-squad github.com/spyzhov/ajson v0.9.6 // @grafana/grafana-sharing-squad github.com/stretchr/testify v1.11.1 // @grafana/grafana-backend-group + github.com/testcontainers/testcontainers-go v0.36.0 //@grafana/grafana-app-platform-squad github.com/thomaspoignant/go-feature-flag v1.42.0 // @grafana/grafana-backend-group - github.com/tjhop/slog-gokit v0.1.3 // @grafana/grafana-app-platform-squad + github.com/tjhop/slog-gokit v0.1.5 // @grafana/grafana-app-platform-squad github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 // @grafana/grafana-backend-group github.com/urfave/cli v1.22.17 // indirect; @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.7 // @grafana/grafana-backend-group - github.com/urfave/cli/v3 v3.4.1 // @grafana/grafana-backend-group + github.com/urfave/cli/v3 v3.5.0 // @grafana/grafana-backend-group github.com/wk8/go-ordered-map v1.0.0 // @grafana/grafana-backend-group github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // @grafana/grafana-operator-experience-squad github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group - go.opencensus.io v0.24.0 // @grafana/grafana-backend-group - go.opentelemetry.io/collector/pdata v1.43.0 // @grafana/grafana-backend-group + go.opentelemetry.io/collector/pdata v1.44.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // @grafana/plugins-platform-backend go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // @grafana/grafana-operator-experience-squad go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // @grafana/sharing-squad @@ -197,19 +197,19 @@ require ( go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage go.uber.org/mock v0.6.0 // @grafana/grafana-operator-experience-squad go.uber.org/zap v1.27.0 // @grafana/identity-access-team - gocloud.dev v0.42.0 // @grafana/grafana-app-platform-squad - gocloud.dev/secrets/hashivault v0.42.0 // @grafana/grafana-operator-experience-squad - golang.org/x/crypto v0.42.0 // @grafana/grafana-backend-group + gocloud.dev v0.43.0 // @grafana/grafana-app-platform-squad + gocloud.dev/secrets/hashivault v0.43.0 // @grafana/grafana-operator-experience-squad + golang.org/x/crypto v0.43.0 // @grafana/grafana-backend-group golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // @grafana/alerting-backend - golang.org/x/mod v0.28.0 // indirect; @grafana/grafana-backend-group - golang.org/x/net v0.45.0 // @grafana/oss-big-tent @grafana/partner-datasources + golang.org/x/mod v0.29.0 // indirect; @grafana/grafana-backend-group + golang.org/x/net v0.46.0 // @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/oauth2 v0.32.0 // @grafana/identity-access-team golang.org/x/sync v0.17.0 // @grafana/alerting-backend golang.org/x/text v0.30.0 // @grafana/grafana-backend-group - golang.org/x/time v0.13.0 // @grafana/grafana-backend-group - golang.org/x/tools v0.37.0 // indirect; @grafana/grafana-as-code + golang.org/x/time v0.14.0 // @grafana/grafana-backend-group + golang.org/x/tools v0.38.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.16.0 // @grafana/oss-big-tent - google.golang.org/api v0.235.0 // @grafana/grafana-backend-group + google.golang.org/api v0.242.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.76.0 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.10 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend @@ -229,7 +229,7 @@ require ( pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // @grafana/grafana-app-platform-squad - xorm.io/builder v0.3.6 // @grafana/grafana-backend-group + xorm.io/builder v0.3.13 // @grafana/grafana-backend-group ) require ( @@ -237,8 +237,10 @@ require ( github.com/grafana/grafana/apps/alerting/alertenrichment v0.0.0 // @grafana/alerting-backend github.com/grafana/grafana/apps/alerting/notifications v0.0.0 // @grafana/alerting-backend github.com/grafana/grafana/apps/alerting/rules v0.0.0 // @grafana/alerting-backend + github.com/grafana/grafana/apps/annotation v0.0.0 // @grafana/grafana-backend-services-squad github.com/grafana/grafana/apps/correlations v0.0.0 // @grafana/datapro github.com/grafana/grafana/apps/dashboard v0.0.0 // @grafana/grafana-app-platform-squad @grafana/dashboards-squad + github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b // @grafana/grafana-app-platform-squad github.com/grafana/grafana/apps/folder v0.0.0 // @grafana/grafana-search-and-storage github.com/grafana/grafana/apps/iam v0.0.0 // @grafana/identity-access-team github.com/grafana/grafana/apps/investigations v0.0.0 // @fcjack @matryer @@ -267,6 +269,7 @@ replace ( github.com/grafana/grafana/apps/alerting/alertenrichment => ./apps/alerting/alertenrichment github.com/grafana/grafana/apps/alerting/notifications => ./apps/alerting/notifications github.com/grafana/grafana/apps/alerting/rules => ./apps/alerting/rules + github.com/grafana/grafana/apps/annotation => ./apps/annotation github.com/grafana/grafana/apps/correlations => ./apps/correlations github.com/grafana/grafana/apps/dashboard => ./apps/dashboard github.com/grafana/grafana/apps/folder => ./apps/folder @@ -289,8 +292,8 @@ replace ( require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go v0.121.1 // indirect - cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go v0.121.4 // indirect + cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect @@ -311,8 +314,8 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/FZambia/eagle v0.2.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -331,23 +334,23 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/at-wat/mqtt-go v0.19.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect - github.com/aws/aws-sdk-go-v2/config v1.31.2 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.18.6 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect - github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect @@ -400,10 +403,9 @@ require ( github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.4.0+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect; @grafana/grafana-app-platform-squad github.com/docker/go-units v0.5.0 // indirect github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect - github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad // indirect github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect github.com/dolthub/maphash v0.1.0 // indirect github.com/edsrzf/mmap-go v1.2.0 // indirect @@ -436,7 +438,6 @@ require ( github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/gomodule/redigo v1.8.9 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.1 // indirect @@ -447,7 +448,6 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect - github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grafana/sqlds/v4 v4.2.7 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect @@ -464,10 +464,10 @@ require ( github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/hashicorp/memberlist v0.5.2 // indirect github.com/hashicorp/serf v0.10.2 // indirect - github.com/hashicorp/vault/api v1.16.0 // indirect + github.com/hashicorp/vault/api v1.20.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect @@ -584,7 +584,6 @@ require ( github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tetratelabs/wazero v1.8.2 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect @@ -607,10 +606,10 @@ require ( go.etcd.io/etcd/client/v3 v3.6.4 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/collector/featuregate v1.43.0 // indirect + go.opentelemetry.io/collector/featuregate v1.44.0 // indirect go.opentelemetry.io/collector/semconv v0.124.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect @@ -619,7 +618,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.59.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect go.opentelemetry.io/otel/log v0.12.2 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect @@ -631,12 +630,12 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/tools/godoc v0.1.0-deprecated // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect @@ -656,7 +655,15 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) +require github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling + require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/swag/conv v0.25.1 // indirect github.com/go-openapi/swag/fileutils v0.25.1 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect @@ -666,6 +673,20 @@ require ( github.com/go-openapi/swag/stringutils v0.25.1 // indirect github.com/go-openapi/swag/typeutils v0.25.1 // indirect github.com/go-openapi/swag/yamlutils v0.25.1 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect ) // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream @@ -679,6 +700,10 @@ exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible // lock for mysql tsdb compat replace github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.1 +// Use our fork of dolthub/go-mysql-server which makes non-cgo the default +// since using a build tag is not sufficient for some use cases (e.g. developers tests in IDE). +replace github.com/dolthub/go-mysql-server => github.com/grafana/go-mysql-server v0.20.1-grafana1 + // v1.* versions were retracted, we need to stick with v0.*. This should work // without the exclude, but this otherwise gets pulled in as a transitive // dependency. diff --git a/go.sum b/go.sum index 4e4f315150e..5717702b7a3 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFO cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= -cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= -cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs= +cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -109,8 +109,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= -cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= +cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= @@ -641,8 +641,12 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 h1:qFYgLH2zZe3WHpQgUrzeazC+ebDebwAQqS9yE1cP5Bs= github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2/go.mod h1:09/ALd1AXCTCOfcJYD8+jIYKmFmi6PVCkTsipC18F7E= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= github.com/Azure/azure-sdk-for-go v23.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -723,12 +727,12 @@ github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYI github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= @@ -842,54 +846,54 @@ github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= -github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= +github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= -github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0= -github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= -github.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU= -github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 h1:6VFPH/Zi9xYFMJKPQOX5URYkQoXRWeJ7V/7Y6ZDYoms= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= +github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4= +github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8= +github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 h1:Nn3qce+OHZuMj/edx4its32uxedAmquCDxtZkrdeiD4= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3/go.mod h1:aqsLGsPs+rJfwDBwWHLcIV8F7AFcikFTPLwUD4RwORQ= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 h1:e5cbPZYTIY2nUEFieZUfVdINOiCTvChOMPfdLnmiLzs= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0/go.mod h1:UseIHRfrm7PqeZo6fcTb6FUCXzCnh1KJbQbmOfxArGM= github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 h1:IfMb3Ar8xEaWjgH/zeVHYD8izwJdQgRP5mKCTDt4GNk= github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= -github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc= -github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.2/go.mod h1:Pqd9k4TuespkireN206cK2QBsaBTL6X+VPAez5Qcijk= github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 h1:teOWtElLARLOhpYWwupjLbY9j5I/yZ/H1I8jg41An78= github.com/aws/aws-sdk-go-v2/service/oam v1.18.3/go.mod h1:wGhpdyftHX6/1U4egowHkYdypwBMjpb+KjAAprv6z20= github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:PwbxovpcJvb25k019bkibvJfCpCmIANOFrXZIFPmRzk= github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4= -github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s= @@ -1049,6 +1053,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -1058,12 +1064,16 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= @@ -1118,22 +1128,20 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= -github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad h1:66ZPawHszNu37VPQckdhX1BPPVzREsGgNxQeefnlm3g= -github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= -github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e h1:7pAttAqWaudUAsM9iHASi/4eFBK+qn4qeaNto7g8bK4= -github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e/go.mod h1:KZyoO3jngyZCLyCf100FEQTrwAHj33AIMj4Zv4u3MNE= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= -github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4 h1:LGTt2LtYX8vaai32d+c9L0sMcP+Dg9w1kO6+lbsxxYg= -github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33 h1:ScHTwNbcVC6JH1OSyXzj8S4w67BIpRXwTSjrac3/PSw= +github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33/go.mod h1:8pvvk5OLaLN9LLxghyczUapn/97l+mBgIb10qC1LG84= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= @@ -1240,8 +1248,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= +github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -1253,6 +1261,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= @@ -1378,8 +1388,6 @@ github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= -github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= @@ -1429,8 +1437,6 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -1580,8 +1586,8 @@ github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38 github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= @@ -1619,18 +1625,20 @@ github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz2 github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/e2e v0.1.1 h1:/b6xcv5BtoBnx8cZnCiey9DbjEc8z7gXHO5edoeRYxc= github.com/grafana/e2e v0.1.1/go.mod h1:RpNLgae5VT+BUHvPE+/zSypmOXKwEu4t+tnEMS1ATaE= +github.com/grafana/go-mysql-server v0.20.1-grafana1 h1:yA4Mzt+tTdIlQutBUaiPnepULPQ7CS4hMu2GOpHqT6s= +github.com/grafana/go-mysql-server v0.20.1-grafana1/go.mod h1:EeYR0apo+8j2Dyxmn2ghkPlirO2S5mT1xHBrA+Efys8= github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKOtTfyD1wXlVsQ2yEXmd0u82h5obs= github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI= github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl6LhqBdbyAQ9YFhExwsj4bjh5vwMNRZY= github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -github.com/grafana/grafana-app-sdk v0.47.0 h1:zTKV+p6zM9y+In+dAcaHczbJJsQj9WKglSBcQXMOA+8= -github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana-aws-sdk v1.2.0 h1:LLR4/g91WBuCRwm2cbWfCREq565+GxIFe08nqqIcIuw= -github.com/grafana/grafana-aws-sdk v1.2.0/go.mod h1:bBo7qOmM3f61vO+2JxTolNUph1l2TmtzmWcU9/Im+8A= +github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= +github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= +github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc= github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 h1:JOzchPgptwJdruYoed7x28lFDwhzs7kssResYsnC0iI= @@ -1641,6 +1649,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 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.281.0 h1:V8dGyatzcOLQeivFhBV2JWMwTSZH/clDnpfKG9p3dTA= github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4= +github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= +github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= @@ -1651,8 +1661,8 @@ github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLm github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000/go.mod h1:/ZklAgE1i4f3Z8uriXwESmCr1VLF8lBGaJspuaGuf78= github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= -github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 h1:cS0SlJGIlZbmDLctNj5vIYGemrJDLy25wwoiIyZWVN8= -github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b h1:rFjoqJFb2KxJ29K9ltuWRSsdA46SbN0GCxoQc36h5kg= +github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= 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.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= @@ -1757,8 +1767,9 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -1778,8 +1789,8 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= -github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= -github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/hashicorp/vault/api v1.20.0 h1:KQMHElgudOsr+IbJgmbjHnCTxEpKs9LnozA1D3nozU4= +github.com/hashicorp/vault/api v1.20.0/go.mod h1:GZ4pcjfzoOWpkJ3ijHNpEoAxKEsBJnVljyTe3jM2Sms= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hetznercloud/hcloud-go/v2 v2.19.1 h1:UU/7h3uc/rdgspM8xkQF7wokmwZXePWDXcLqrQRRzzY= @@ -1850,8 +1861,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= @@ -1943,6 +1954,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linode/linodego v1.47.0 h1:6MFNCyzWbr8Rhl4r7d5DwZLwxvFIsM4ARH6W0KS/R0U= github.com/linode/linodego v1.47.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= @@ -1954,6 +1967,8 @@ github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -1997,6 +2012,7 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -2066,12 +2082,20 @@ github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QR github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/mocktools/go-smtp-mock/v2 v2.5.1 h1:QcMJMChSgG1olVj4o6xxQFdrWzRjYNrcq660HAjd0wA= @@ -2218,6 +2242,8 @@ 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng= github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -2365,6 +2391,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3/go.mod h1:aJIMhRsunltJR926EB2MUg8qHemFQDreSB33pyto2Ps= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -2461,8 +2489,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= -github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00= +github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0= github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1 h1:z0v9BB/p7s4J6R//+0a5M3wCld8KzNjrGRLIwXfrAZk= github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1/go.mod h1:3ukSkG4rIRUGkKM4oIz+BSuUx2e3RlQVVv3Cc3W+Tv4= github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= @@ -2471,8 +2499,12 @@ github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o= -github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= +github.com/tjhop/slog-gokit v0.1.5 h1:ayloIUi5EK2QYB8eY4DOPO95/mRtMW42lUkp3quJohc= +github.com/tjhop/slog-gokit v0.1.5/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= @@ -2498,8 +2530,8 @@ github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= -github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw= +github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= @@ -2549,6 +2581,8 @@ github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBU github.com/yuin/gopher-lua v0.0.0-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= @@ -2592,23 +2626,22 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc= -go.opentelemetry.io/collector/featuregate v1.43.0 h1:Aq8UR5qv1zNlbbkTyqv8kLJtnoQMq/sG1/jS9o1cCJI= -go.opentelemetry.io/collector/featuregate v1.43.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= -go.opentelemetry.io/collector/pdata v1.43.0 h1:zVkj2hcjiMLwX+QDDNwb7iTh3LBjNXKv2qPSgj1Rzb4= -go.opentelemetry.io/collector/pdata v1.43.0/go.mod h1:KsJzdDG9e5BaHlmYr0sqdSEKeEiSfKzoF+rdWU7J//w= +go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= +go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/pdata v1.44.0 h1:q/EfWDDKrSaf4hjTIzyPeg1ZcCRg1Uj7VTFnGfNVdk8= +go.opentelemetry.io/collector/pdata v1.44.0/go.mod h1:LnsjYysFc3AwMVh6KGNlkGKJUF2ReuWxtD9Hb3lSMZk= go.opentelemetry.io/collector/pdata/pprofile v0.124.0 h1:ZjL9wKqzP4BHj0/F1jfGxs1Va8B7xmYayipZeNVoWJE= go.opentelemetry.io/collector/pdata/pprofile v0.124.0/go.mod h1:1EN3Gw5LSI4fSVma/Yfv/6nqeuYgRTm1/kmG5nE5Oyo= go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/9dNZ0rv454goA= go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= +go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -2648,8 +2681,8 @@ go.opentelemetry.io/otel/exporters/prometheus v0.59.0 h1:HHf+wKS6o5++XZhS98wvILr go.opentelemetry.io/otel/exporters/prometheus v0.59.0/go.mod h1:R8GpRXTZrqvXHDEGVH5bF6+JqAZcK8PjJcZ5nGhEWiE= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJhSENRg+Vvx+ynNilV8twBLBsXMY= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= @@ -2706,10 +2739,10 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= -gocloud.dev v0.42.0 h1:qzG+9ItUL3RPB62/Amugws28n+4vGZXEoJEAMfjutzw= -gocloud.dev v0.42.0/go.mod h1:zkaYAapZfQisXOA4bzhsbA4ckiStGQ3Psvs9/OQ5dPM= -gocloud.dev/secrets/hashivault v0.42.0 h1:kPWIIu1AP6ApHf7HwUrzGrwyRJEfTd46JNUNgnbpsFA= -gocloud.dev/secrets/hashivault v0.42.0/go.mod h1:LXprr1XLEAT7BVZ+Y66dJEHQMzDsowIExj5Ktr9HLvM= +gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M= +gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4= +gocloud.dev/secrets/hashivault v0.43.0 h1:A966rEMpCRUE9209/+k+A2HP2v2qDnrxGpQn+nIH5uY= +gocloud.dev/secrets/hashivault v0.43.0/go.mod h1:KdWKL+TXDi0cXgEd/MTeaidKlotvyJtnTDi71B3rR9U= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -2751,8 +2784,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2815,8 +2848,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2902,8 +2935,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 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.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2998,6 +3031,7 @@ golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -3030,6 +3064,7 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -3102,8 +3137,8 @@ golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -3122,8 +3157,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -3154,8 +3189,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -3232,8 +3267,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -3324,8 +3359,8 @@ google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjY google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= -google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= -google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= +google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -3476,8 +3511,8 @@ google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= +google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -3612,8 +3647,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= -gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -3734,5 +3769,5 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= -xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= +xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= +xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= diff --git a/go.work b/go.work index 1b60b90eae9..c1baa393967 100644 --- a/go.work +++ b/go.work @@ -9,8 +9,10 @@ use ( ./apps/alerting/alertenrichment ./apps/alerting/notifications ./apps/alerting/rules + ./apps/annotation ./apps/correlations ./apps/dashboard + ./apps/example ./apps/folder ./apps/iam ./apps/investigations diff --git a/go.work.sum b/go.work.sum index 17c8fc4b45a..be369dac104 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,5 +1,3 @@ -atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= -atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= @@ -12,482 +10,243 @@ buf.build/go/protovalidate v0.12.0 h1:4GKJotbspQjRCcqZMGVSuC8SjwZ/FmgtSuKDpKUTZe buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= -cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= -cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go v0.118.1/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M= -cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= -cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go v0.82.0/go.mod h1:vlKccHJGuFBFufnAnuB08dfEH9Y3H7dzDzRECFdC2TA= cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= -cloud.google.com/go/accessapproval v1.8.3 h1:axlU03FRiXDNupsmPG7LKzuS4Enk1gf598M62lWVB74= -cloud.google.com/go/accessapproval v1.8.3/go.mod h1:3speETyAv63TDrDmo5lIkpVueFkQcQchkiw/TAMbBo4= -cloud.google.com/go/accessapproval v1.8.6 h1:UkmDPCKvj24bkGVrvgJPcgSDkmIPw/bAmOiDb9avOiE= -cloud.google.com/go/accessapproval v1.8.6/go.mod h1:FfmTs7Emex5UvfnnpMkhuNkRCP85URnBFt5ClLxhZaQ= -cloud.google.com/go/accesscontextmanager v1.9.3 h1:8zVoeiBa4erMCLEXltOcqVEsZhS26JZ5/Vrgs59eQiI= -cloud.google.com/go/accesscontextmanager v1.9.3/go.mod h1:S1MEQV5YjkAKBoMekpGrkXKfrBdsi4x6Dybfq6gZ8BU= +cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/accessapproval v1.8.7 h1:Sc9ZjxFBEM/PoAxNlUwVGDcv8DYyjLYWDxHlzPG0q5I= +cloud.google.com/go/accessapproval v1.8.7/go.mod h1:BFvZOW4GJjJnl6aA/YDEg0TGViFHyusa/bMdcVFmh8A= cloud.google.com/go/accesscontextmanager v1.9.6 h1:2LnncRqfYB8NEdh9+FeYxAt9POTW/0zVboktnRlO11w= cloud.google.com/go/accesscontextmanager v1.9.6/go.mod h1:884XHwy1AQpCX5Cj2VqYse77gfLaq9f8emE2bYriilk= -cloud.google.com/go/aiplatform v1.74.0 h1:rE2P5H7FOAFISAZilmdkapbk4CVgwfVs6FDWlhGfuy0= -cloud.google.com/go/aiplatform v1.74.0/go.mod h1:hVEw30CetNut5FrblYd1AJUWRVSIjoyIvp0EVUh51HA= -cloud.google.com/go/aiplatform v1.89.0 h1:niSJYc6ldWWVM9faXPo1Et1MVSQoLvVGriD7fwbJdtE= -cloud.google.com/go/aiplatform v1.89.0/go.mod h1:TzZtegPkinfXTtXVvZZpxx7noINFMVDrLkE7cEWhYEk= -cloud.google.com/go/analytics v0.26.0 h1:O2kWr2Sd4ep3I+YJ4aiY0G4+zWz6sp4eTce+JVns9TM= -cloud.google.com/go/analytics v0.26.0/go.mod h1:KZWJfs8uX/+lTjdIjvT58SFa86V9KM6aPXwZKK6uNVI= -cloud.google.com/go/analytics v0.28.1 h1:W2ft49J/LeEj9A07Jsd5Q2kAzajK0j0IffOyyzbxw04= -cloud.google.com/go/analytics v0.28.1/go.mod h1:iPaIVr5iXPB3JzkKPW1JddswksACRFl3NSHgVHsuYC4= -cloud.google.com/go/apigateway v1.7.3 h1:Mn7cC5iWJz+cSMS/Hb+N2410CpZ6c8XpJKaexBl0Gxs= -cloud.google.com/go/apigateway v1.7.3/go.mod h1:uK0iRHdl2rdTe79bHW/bTsKhhXPcFihjUdb7RzhTPf4= -cloud.google.com/go/apigateway v1.7.6 h1:do+u3rjDYuTxD2ypRfv4uwTMoy/VHFLclvaYcb5Mv6I= -cloud.google.com/go/apigateway v1.7.6/go.mod h1:SiBx36VPjShaOCk8Emf63M2t2c1yF+I7mYZaId7OHiA= -cloud.google.com/go/apigeeconnect v1.7.3 h1:Wlr+30Tha0SMCvQYZKdrh+HkpOyl0CQFSlzeY/Gg1gs= -cloud.google.com/go/apigeeconnect v1.7.3/go.mod h1:2ZkT5VCAqhYrDqf4dz7lGp4N/+LeNBSfou8Qs5bIuSg= -cloud.google.com/go/apigeeconnect v1.7.6 h1:ijEJSni5xROOn1YyiHgqcW0B0TWr0di9VgIi2gvyNjY= -cloud.google.com/go/apigeeconnect v1.7.6/go.mod h1:zqDhHY99YSn2li6OeEjFpAlhXYnXKl6DFb/fGu0ye2w= -cloud.google.com/go/apigeeregistry v0.9.3 h1:j9CJg/oC884OX5cDpiwNt1ZlDXNV6Zb9Mp1YmRrOG0k= -cloud.google.com/go/apigeeregistry v0.9.3/go.mod h1:oNCP2VjOeI6U8yuOuTmU4pkffdcXzR5KxeUD71gF+Dg= +cloud.google.com/go/aiplatform v1.93.0 h1:RaW2Gp/ywcMRVzNEbriviQScRClKUWNZTBaWpthZhvo= +cloud.google.com/go/aiplatform v1.93.0/go.mod h1:4lKl6YhQz5V+zewJ+RsqQ91H7hjy2AmDy+93ZhwSgiE= +cloud.google.com/go/analytics v0.29.0 h1:oYiABctb2wxUs1khR6vpC/T4P1EN27yvKI6Fxqzuv8E= +cloud.google.com/go/analytics v0.29.0/go.mod h1:NysnqKYB3101TBxuyEciW+wxmcGn44tmbq/pu9IsHcY= +cloud.google.com/go/apigateway v1.7.7 h1:ehKUTy+QFsb3n07fEi18S2dpDDjCV4UlRyrbwfZV3Zk= +cloud.google.com/go/apigateway v1.7.7/go.mod h1:j1bCmrUK1BzVHpiIyTApxB7cRyhivKzltqLmp6j6i7U= +cloud.google.com/go/apigeeconnect v1.7.7 h1:S6s2zojwMymx0fyZYKm0eK1TdDxrriIBAlNVvRAOzug= +cloud.google.com/go/apigeeconnect v1.7.7/go.mod h1:ftGK3nca0JePiVLl0A6alaMjKdOc5C+sAkFMyH2RH8U= cloud.google.com/go/apigeeregistry v0.9.6 h1:TgdjAoGoRY81DEc2LYsYvi/OqCFImMzAk/TVKiSRsQw= cloud.google.com/go/apigeeregistry v0.9.6/go.mod h1:AFEepJBKPtGDfgabG2HWaLH453VVWWFFs3P4W00jbPs= cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= -cloud.google.com/go/appengine v1.9.3 h1:jrcanSzj9J1erevZuxldvsDwY+0k/DeFFzlnSfPGfL8= -cloud.google.com/go/appengine v1.9.3/go.mod h1:DtLsE/z3JufM/pCEIyVYebJ0h9UNPpN64GZQrYgOSyM= -cloud.google.com/go/appengine v1.9.6 h1:JJyY8icMmQeWfQ+d36IhkGvd3Guzvw0UAkvxT0wmUx8= -cloud.google.com/go/appengine v1.9.6/go.mod h1:jPp9T7Opvzl97qytaRGPwoH7pFI3GAcLDaui1K8PNjY= -cloud.google.com/go/area120 v0.9.3 h1:dPQ07rW4eku8OgNWDOaQaVGcE4+XfhH8BSbVwdVQ+wU= -cloud.google.com/go/area120 v0.9.3/go.mod h1:F3vxS/+hqzrjJo55Xvda3Jznjjbd+4Foo43SN5eMd8M= -cloud.google.com/go/area120 v0.9.6 h1:iJrZ6AleZr4l+q0/fWVANFOhs90KiSB1Ccait5OYyNg= -cloud.google.com/go/area120 v0.9.6/go.mod h1:qKSokqe0iTmwBDA3tbLWonMEnh0pMAH4YxiceiHUed4= -cloud.google.com/go/artifactregistry v1.16.1 h1:ZNXGB6+T7VmWdf6//VqxLdZ/sk0no8W0ujanHeJwDRw= -cloud.google.com/go/artifactregistry v1.16.1/go.mod h1:sPvFPZhfMavpiongKwfg93EOwJ18Tnj9DIwTU9xWUgs= +cloud.google.com/go/appengine v1.9.7 h1:IxGz6j5xv0nTJX285wu95Vn6KEi2CeV9vbyRgCSEAoU= +cloud.google.com/go/appengine v1.9.7/go.mod h1:y1XpGVeAhbsNzHida79cHbr3pFRsym0ob8xnC8yphbo= +cloud.google.com/go/area120 v0.9.7 h1:BbpzLwaIXVPorrrzTH+ni7P5mLemmPPfSZ7o39k7zQc= +cloud.google.com/go/area120 v0.9.7/go.mod h1:5nJ0yksmjOMfc4Zpk+okWfJ3A1004FvB82rfia+ZLaY= cloud.google.com/go/artifactregistry v1.17.1 h1:A20kj2S2HO9vlyBVyVFHPxArjxkXvLP5LjcdE7NhaPc= cloud.google.com/go/artifactregistry v1.17.1/go.mod h1:06gLv5QwQPWtaudI2fWO37gfwwRUHwxm3gA8Fe568Hc= -cloud.google.com/go/asset v1.20.4 h1:6oNgjcs5KCPGBD71G0IccK6TfeFsEtBTyQ3Q+Dn09bs= -cloud.google.com/go/asset v1.20.4/go.mod h1:DP09pZ+SoFWUZyPZx26xVroHk+6+9umnQv+01yfJxbM= cloud.google.com/go/asset v1.21.1 h1:i55wWC/EwVdHMyJgRfbLp/L6ez4nQuOpZwSxkuqN9ek= cloud.google.com/go/asset v1.21.1/go.mod h1:7AzY1GCC+s1O73yzLM1IpHFLHz3ws2OigmCpOQHwebk= -cloud.google.com/go/assuredworkloads v1.12.3 h1:RU1WhF1zMggdXAZ+ezYTn4Eh/FdiX7sz8lLXGERn4Po= -cloud.google.com/go/assuredworkloads v1.12.3/go.mod h1:iGBkyMGdtlsxhCi4Ys5SeuvIrPTeI6HeuEJt7qJgJT8= cloud.google.com/go/assuredworkloads v1.12.6 h1:ip/shfJYx6lrHBWYADjrrrubcm7uZzy50TTF5tPG7ek= cloud.google.com/go/assuredworkloads v1.12.6/go.mod h1:QyZHd7nH08fmZ+G4ElihV1zoZ7H0FQCpgS0YWtwjCKo= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= -cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= -cloud.google.com/go/automl v1.14.4 h1:vkD+hQ75SMINMgJBT/KDpFYvfQLzJbtIQZdw0AWq8Rs= -cloud.google.com/go/automl v1.14.4/go.mod h1:sVfsJ+g46y7QiQXpVs9nZ/h8ntdujHm5xhjHW32b3n4= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/automl v1.14.7 h1:ZLj48Ur2Qcso4M3bgOtjsOmeV5Ee92N14wuOc8OW+L0= cloud.google.com/go/automl v1.14.7/go.mod h1:8a4XbIH5pdvrReOU72oB+H3pOw2JBxo9XTk39oljObE= -cloud.google.com/go/baremetalsolution v1.3.3 h1:OL+KT+wCumdDhG44aeqGAdkwdT8Wa4Lh+o4INM+CQjw= -cloud.google.com/go/baremetalsolution v1.3.3/go.mod h1:uF9g08RfmXTF6ZKbXxixy5cGMGFcG6137Z99XjxLOUI= cloud.google.com/go/baremetalsolution v1.3.6 h1:9bdGlpY1LgLONQjFsDwrkjLzdPTlROpfU+GhA97YpOk= cloud.google.com/go/baremetalsolution v1.3.6/go.mod h1:7/CS0LzpLccRGO0HL3q2Rofxas2JwjREKut414sE9iM= -cloud.google.com/go/batch v1.12.0 h1:lXuTaELvU0P0ARbTFxxdpOC/dFnZZeGglSw06BtO//8= -cloud.google.com/go/batch v1.12.0/go.mod h1:CATSBh/JglNv+tEU/x21Z47zNatLQ/gpGnpyKOzbbcM= cloud.google.com/go/batch v1.12.2 h1:gWQdvdPplptpvrkqF6ibtxZkOsYKLTFbxYawHa/TvCg= cloud.google.com/go/batch v1.12.2/go.mod h1:tbnuTN/Iw59/n1yjAYKV2aZUjvMM2VJqAgvUgft6UEU= -cloud.google.com/go/beyondcorp v1.1.3 h1:ezavJc0Gzh4N8zBskO/DnUVMWPa8lqH/tmQSyaknmCA= -cloud.google.com/go/beyondcorp v1.1.3/go.mod h1:3SlVKnlczNTSQFuH5SSyLuRd4KaBSc8FH/911TuF/Cc= cloud.google.com/go/beyondcorp v1.1.6 h1:4FcR+4QmcNGkhVij6TrYS4AQVNLBo7PBXKxNrKzpclQ= cloud.google.com/go/beyondcorp v1.1.6/go.mod h1:V1PigSWPGh5L/vRRmyutfnjAbkxLI2aWqJDdxKbwvsQ= -cloud.google.com/go/bigquery v1.66.2 h1:EKOSqjtO7jPpJoEzDmRctGea3c2EOGoexy8VyY9dNro= -cloud.google.com/go/bigquery v1.66.2/go.mod h1:+Yd6dRyW8D/FYEjUGodIbu0QaoEmgav7Lwhotup6njo= cloud.google.com/go/bigquery v1.69.0 h1:rZvHnjSUs5sHK3F9awiuFk2PeOaB8suqNuim21GbaTc= cloud.google.com/go/bigquery v1.69.0/go.mod h1:TdGLquA3h/mGg+McX+GsqG9afAzTAcldMjqhdjHTLew= -cloud.google.com/go/bigtable v1.35.0 h1:UEacPwaejN2mNbz67i1Iy3G812rxtgcs6ePj1TAg7dw= -cloud.google.com/go/bigtable v1.35.0/go.mod h1:EabtwwmTcOJFXp+oMZAT/jZkyDIjNwrv53TrS4DGrrM= -cloud.google.com/go/bigtable v1.37.0 h1:Q+x7y04lQ0B+WXp03wc1/FLhFt4CwcQdkwWT0M4Jp3w= -cloud.google.com/go/bigtable v1.37.0/go.mod h1:HXqddP6hduwzrtiTCqZPpj9ij4hGZb4Zy1WF/dT+yaU= -cloud.google.com/go/billing v1.20.1 h1:xMlO3hc5BI0s23tRB40bL40xSpxUR1x3E07Y5/VWcjU= -cloud.google.com/go/billing v1.20.1/go.mod h1:DhT80hUZ9gz5UqaxtK/LNoDELfxH73704VTce+JZqrY= +cloud.google.com/go/bigtable v1.38.0 h1:L/PnUXRtAzFfa7qMULJHt4cXa/O2dqPJEkzYNGA4hfo= +cloud.google.com/go/bigtable v1.38.0/go.mod h1:o/lntJarF3Y5C0XYLMJLjLYwxaRbcrtM0BiV57ymXbI= cloud.google.com/go/billing v1.20.4 h1:pqM5/c9UGydB9H90IPCxSvfCNLUPazAOSMsZkz5q5P4= cloud.google.com/go/billing v1.20.4/go.mod h1:hBm7iUmGKGCnBm6Wp439YgEdt+OnefEq/Ib9SlJYxIU= -cloud.google.com/go/binaryauthorization v1.9.3 h1:X8JRfmk0/vyRqLusEyAPr0nZCK6RKae9omB4lrit0XI= -cloud.google.com/go/binaryauthorization v1.9.3/go.mod h1:f3xcb/7vWklDoF+q2EaAIS+/A/e1278IgiYxonRX+Jk= cloud.google.com/go/binaryauthorization v1.9.5 h1:T0zYEroXT+y0O/x/yZd5SwQdFv4UbUINjvJyJKzDm0Q= cloud.google.com/go/binaryauthorization v1.9.5/go.mod h1:CV5GkS2eiY461Bzv+OH3r5/AsuB6zny+MruRju3ccB8= -cloud.google.com/go/certificatemanager v1.9.3 h1:2UP31fg7b+y3F0OmNbPHOKPEJ+6LOMfxAXX4p8xGCy4= -cloud.google.com/go/certificatemanager v1.9.3/go.mod h1:O5T4Lg/dHbDHLFFooV2Mh/VsT3Mj2CzPEWRo4qw5prc= cloud.google.com/go/certificatemanager v1.9.5 h1:+ZPglfDurCcsv4azizDFpBucD1IkRjWjbnU7zceyjfY= cloud.google.com/go/certificatemanager v1.9.5/go.mod h1:kn7gxT/80oVGhjL8rurMUYD36AOimgtzSBPadtAeffs= -cloud.google.com/go/channel v1.19.2 h1:oHyO3QAZ6kdf6SwqnUTBz50ND6Nk2rxZtboUiF4dgLE= -cloud.google.com/go/channel v1.19.2/go.mod h1:syX5opXGXFt17DHCyCdbdlM464Tx0gHMi46UlEWY9Gg= -cloud.google.com/go/channel v1.19.5 h1:UI+ZsRkS15hi9DRF+WAvTVLVuSeZiRmvCU8cjkjOwUU= -cloud.google.com/go/channel v1.19.5/go.mod h1:vevu+LK8Oy1Yuf7lcpDbkQQQm5I7oiY5fFTn3uwfQLY= -cloud.google.com/go/cloudbuild v1.22.0 h1:zmDznviZpvkCla0adbp7jJsMYZ9bABCbcPK2cBUHwg8= -cloud.google.com/go/cloudbuild v1.22.0/go.mod h1:p99MbQrzcENHb/MqU3R6rpqFRk/X+lNG3PdZEIhM95Y= +cloud.google.com/go/channel v1.20.0 h1:EeUa6SnD3+EL9B06G6N9Ud5/p/NtT6PC7lv5kmaUiHs= +cloud.google.com/go/channel v1.20.0/go.mod h1:nBR1Lz+/1TjSA16HTllvW9Y+QULODj3o3jEKrNNeOp4= cloud.google.com/go/cloudbuild v1.22.2 h1:4LlrIFa3IFLgD1mGEXmUE4cm9fYoU71OLwTvjM7Dg3c= cloud.google.com/go/cloudbuild v1.22.2/go.mod h1:rPyXfINSgMqMZvuTk1DbZcbKYtvbYF/i9IXQ7eeEMIM= -cloud.google.com/go/clouddms v1.8.4 h1:CDOd1nwmP4uek+nZhl4bhRIpzj8jMqoMRqKAfKlgLhw= -cloud.google.com/go/clouddms v1.8.4/go.mod h1:RadeJ3KozRwy4K/gAs7W74ZU3GmGgVq5K8sRqNs3HfA= cloud.google.com/go/clouddms v1.8.7 h1:IWJbQBEECTaNanDRN1XdR7FU53MJ1nylTl3s9T3MuyI= cloud.google.com/go/clouddms v1.8.7/go.mod h1:DhWLd3nzHP8GoHkA6hOhso0R9Iou+IGggNqlVaq/KZ4= -cloud.google.com/go/cloudtasks v1.13.3 h1:rXdznKjCa7WpzmvR2plrn2KJ+RZC1oYxPiRWNQjjf3k= -cloud.google.com/go/cloudtasks v1.13.3/go.mod h1:f9XRvmuFTm3VhIKzkzLCPyINSU3rjjvFUsFVGR5wi24= cloud.google.com/go/cloudtasks v1.13.6 h1:Fwan19UiNoFD+3KY0MnNHE5DyixOxNzS1mZ4ChOdpy0= cloud.google.com/go/cloudtasks v1.13.6/go.mod h1:/IDaQqGKMixD+ayM43CfsvWF2k36GeomEuy9gL4gLmU= -cloud.google.com/go/compute v1.34.0 h1:+k/kmViu4TEi97NGaxAATYtpYBviOWJySPZ+ekA95kk= -cloud.google.com/go/compute v1.34.0/go.mod h1:zWZwtLwZQyonEvIQBuIa0WvraMYK69J5eDCOw9VZU4g= -cloud.google.com/go/compute v1.38.0 h1:MilCLYQW2m7Dku8hRIIKo4r0oKastlD74sSu16riYKs= -cloud.google.com/go/compute v1.38.0/go.mod h1:oAFNIuXOmXbK/ssXm3z4nZB8ckPdjltJ7xhHCdbWFZM= +cloud.google.com/go/compute v1.40.0 h1:dlEzKo/BtyEGNc+SflXwwoBh52dNl/A5BaSYurT0k0k= +cloud.google.com/go/compute v1.40.0/go.mod h1:P1doTJnlwurJDzIQFMp4mgU+vyCe9HU2NWTlqTfq3MY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/contactcenterinsights v1.17.1 h1:xJoZbX0HM1zht8KxAB38hs2v4Hcl+vXGLo454LrdwxA= -cloud.google.com/go/contactcenterinsights v1.17.1/go.mod h1:n8OiNv7buLA2AkGVkfuvtW3HU13AdTmEwAlAu46bfxY= cloud.google.com/go/contactcenterinsights v1.17.3 h1:lenyU3uzHwKDveCwmpfNxHYvLS3uEBWdn+O7+rSxy+Q= cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8= -cloud.google.com/go/container v1.42.2 h1:8ncSEBjkng6ucCICauaUGzBomoM2VyYzleAum1OFcow= -cloud.google.com/go/container v1.42.2/go.mod h1:y71YW7uR5Ck+9Vsbst0AF2F3UMgqmsN4SP8JR9xEsR8= cloud.google.com/go/container v1.43.0 h1:A6J92FJPfxTvyX7MHF+w4t2W9WCqvHOi9UB5SAeSy3w= cloud.google.com/go/container v1.43.0/go.mod h1:ETU9WZ1KM9ikEKLzrhRVao7KHtalDQu6aPqM34zDr/U= -cloud.google.com/go/containeranalysis v0.13.3 h1:1D8U75BeotZxrG4jR6NYBtOt+uAeBsWhpBZmSYLakQw= -cloud.google.com/go/containeranalysis v0.13.3/go.mod h1:0SYnagA1Ivb7qPqKNYPkCtphhkJn3IzgaSp3mj+9XAY= cloud.google.com/go/containeranalysis v0.14.1 h1:1SoHlNqL3XrhqcoozB+3eoHif2sRUFtp/JeASQTtGKo= cloud.google.com/go/containeranalysis v0.14.1/go.mod h1:28e+tlZgauWGHmEbnI5UfIsjMmrkoR1tFN0K2i71jBI= -cloud.google.com/go/datacatalog v1.24.3 h1:3bAfstDB6rlHyK0TvqxEwaeOvoN9UgCs2bn03+VXmss= -cloud.google.com/go/datacatalog v1.24.3/go.mod h1:Z4g33XblDxWGHngDzcpfeOU0b1ERlDPTuQoYG6NkF1s= cloud.google.com/go/datacatalog v1.26.0 h1:eFgygb3DTufTWWUB8ARk+dSuXz+aefNJXTlkWlQcWwE= cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14= -cloud.google.com/go/dataflow v0.10.3 h1:+7IfIXzYWSybIIDGK9FN2uqBsP/5b/Y0pBYzNhcmKSU= -cloud.google.com/go/dataflow v0.10.3/go.mod h1:5EuVGDh5Tg4mDePWXMMGAG6QYAQhLNyzxdNQ0A1FfW4= cloud.google.com/go/dataflow v0.11.0 h1:AdhB4cAkMOC9NtrHJxpKOVvO/VqBLaIyk0tEEhbGjYM= cloud.google.com/go/dataflow v0.11.0/go.mod h1:gNHC9fUjlV9miu0hd4oQaXibIuVYTQvZhMdPievKsPk= -cloud.google.com/go/dataform v0.10.3 h1:ZpGkZV8OyhUhvN/tfLffU2ki5ERTtqOunkIaiVAhmw0= -cloud.google.com/go/dataform v0.10.3/go.mod h1:8SruzxHYCxtvG53gXqDZvZCx12BlsUchuV/JQFtyTCw= cloud.google.com/go/dataform v0.12.0 h1:0eCPTPUC/RZ863aVfXTJLkg0tEpdpn62VD6ywSmmzxM= cloud.google.com/go/dataform v0.12.0/go.mod h1:PuDIEY0lSVuPrZqcFji1fmr5RRvz3DGz4YP/cONc8g4= -cloud.google.com/go/datafusion v1.8.3 h1:FTMtsf2nfGGlDCuE84/RvVaCcTIYE7WQSB0noeO0cwI= -cloud.google.com/go/datafusion v1.8.3/go.mod h1:hyglMzE57KRf0Rf/N2VRPcHCwKfZAAucx+LATY6Jc6Q= cloud.google.com/go/datafusion v1.8.6 h1:GZ6J+CR8CEeWAj8luRCtr8GvImSQRkArIIqGiZOnzBA= cloud.google.com/go/datafusion v1.8.6/go.mod h1:fCyKJF2zUKC+O3hc2F9ja5EUCAbT4zcH692z8HiFZFw= -cloud.google.com/go/datalabeling v0.9.3 h1:PqoA3gnOWaLcHCnqoZe4jh3jmiv6+Z7W2xUUkw/j4jE= -cloud.google.com/go/datalabeling v0.9.3/go.mod h1:3LDFUgOx+EuNUzDyjU7VElO8L+b5LeaZEFA/ZU1O1XU= cloud.google.com/go/datalabeling v0.9.6 h1:VOZ5U+78ttnhNCEID7qdeogqZQzK5N+LPHIQ9Q3YDsc= cloud.google.com/go/datalabeling v0.9.6/go.mod h1:n7o4x0vtPensZOoFwFa4UfZgkSZm8Qs0Pg/T3kQjXSM= -cloud.google.com/go/dataplex v1.22.0 h1:j4hD6opb+gq9CJNPFIlIggoW8Kjymg8Wmy2mdHmQoiw= -cloud.google.com/go/dataplex v1.22.0/go.mod h1:g166QMCGHvwc3qlTG4p34n+lHwu7JFfaNpMfI2uO7b8= -cloud.google.com/go/dataplex v1.25.3 h1:Xr0Toh6wyBlmL3H4EPu1YKwxUtkDSzzq+IP0iLc88kk= -cloud.google.com/go/dataplex v1.25.3/go.mod h1:wOJXnOg6bem0tyslu4hZBTncfqcPNDpYGKzed3+bd+E= +cloud.google.com/go/dataplex v1.26.0 h1:nu8/KrLR5v62L1lApGNgm61Oq+xaa2bS9rgc1csjqE0= +cloud.google.com/go/dataplex v1.26.0/go.mod h1:12R9nlLUzxOscbb2HgoYnkGNibmv4sXEVMXxrdw2a90= cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= -cloud.google.com/go/dataproc/v2 v2.11.0 h1:6aRpyoRfNOP+r2+pGb7HeHtF+SYQID8kzztfHuK0plk= -cloud.google.com/go/dataproc/v2 v2.11.0/go.mod h1:9vgGrn57ra7KBqz+B2KD+ltzEXvnHAUClFgq/ryU99g= -cloud.google.com/go/dataproc/v2 v2.11.2 h1:KhC8wdLILpAs17yeTG6Miwg1v0nOP/OXD+9QNg3w6AQ= -cloud.google.com/go/dataproc/v2 v2.11.2/go.mod h1:xwukBjtfiO4vMEa1VdqyFLqJmcv7t3lo+PbLDcTEw+g= -cloud.google.com/go/dataqna v0.9.3 h1:lGUj2FYs650EUPDMV6plWBAoh8qH9Bu1KCz1PUYF2VY= -cloud.google.com/go/dataqna v0.9.3/go.mod h1:PiAfkXxa2LZYxMnOWVYWz3KgY7txdFg9HEMQPb4u1JA= +cloud.google.com/go/dataproc/v2 v2.14.0 h1:oiEM2efaJfiOClBOYcmW1K+tDP5pHdnYsPmfD55tiRw= +cloud.google.com/go/dataproc/v2 v2.14.0/go.mod h1:AqfdObN5w70H7meRXZOEY52WMK4yMrLtiOd9kROahSM= cloud.google.com/go/dataqna v0.9.7 h1:qTRAG/E3T63Xj1orefRlwupfwH9c9ERUAnWSRGp75so= cloud.google.com/go/dataqna v0.9.7/go.mod h1:4ac3r7zm7Wqm8NAc8sDIDM0v7Dz7d1e/1Ka1yMFanUM= cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= -cloud.google.com/go/datastream v1.13.0 h1:C5AeEdze55feJVb17a40QmlnyH/aMhn/uf3Go3hIqPA= -cloud.google.com/go/datastream v1.13.0/go.mod h1:GrL2+KC8mV4GjbVG43Syo5yyDXp3EH+t6N2HnZb1GOQ= cloud.google.com/go/datastream v1.14.1 h1:j+y0lUKm9pbDjJn0YcWxPI/hXNGUQ80GE6yrFuJC/JA= cloud.google.com/go/datastream v1.14.1/go.mod h1:JqMKXq/e0OMkEgfYe0nP+lDye5G2IhIlmencWxmesMo= -cloud.google.com/go/deploy v1.26.2 h1:1c2Cd3jdb0mrKHHfyzSQ5DRmxgYd07tIZZzuMNrwDxU= -cloud.google.com/go/deploy v1.26.2/go.mod h1:XpS3sG/ivkXCfzbzJXY9DXTeCJ5r68gIyeOgVGxGNEs= cloud.google.com/go/deploy v1.27.2 h1:C0VqBhFyQFp6+xgPHZAD7LeRA4XGy5YLzGmPQ2NhlLk= cloud.google.com/go/deploy v1.27.2/go.mod h1:4NHWE7ENry2A4O1i/4iAPfXHnJCZ01xckAKpZQwhg1M= -cloud.google.com/go/dialogflow v1.66.0 h1:/kfpZw20/3v4sC8czEIuvn3Bu3qOne5aHDYlRYHbu18= -cloud.google.com/go/dialogflow v1.66.0/go.mod h1:BPiRTnnXP/tHLot5h/U62Xcp+i6ekRj/bq6uq88p+Lw= -cloud.google.com/go/dialogflow v1.68.2 h1:bXpoqPRf37KKxB79PKr20B/TAU/Z5iA0FnB6C5N2jrA= -cloud.google.com/go/dialogflow v1.68.2/go.mod h1:E0Ocrhf5/nANZzBju8RX8rONf0PuIvz2fVj3XkbAhiY= -cloud.google.com/go/dlp v1.21.0 h1:9kz7+gaB/0gBZsDUnNT1asDihNZSrRFSeUTBcBdUAkk= -cloud.google.com/go/dlp v1.21.0/go.mod h1:Y9HOVtPoArpL9sI1O33aN/vK9QRwDERU9PEJJfM8DvE= -cloud.google.com/go/dlp v1.23.0 h1:3xWRKylXxhysaQaV+DLev1YcIywFUCc7yJEE6R7ZGDQ= -cloud.google.com/go/dlp v1.23.0/go.mod h1:vVT4RlyPMEMcVHexdPT6iMVac3seq3l6b8UPdYpgFrg= -cloud.google.com/go/documentai v1.35.2 h1:hswVobCWUTXtmn+4QqUIVkai7sDOe0QS2KB3IpqLkik= -cloud.google.com/go/documentai v1.35.2/go.mod h1:oh/0YXosgEq3hVhyH4ZQ7VNXPaveRO4eLVM3tBSZOsI= +cloud.google.com/go/dialogflow v1.69.0 h1:nW3vH/ysZWBdjQJ4rIh3PC5Do/Brz8KEp3OeDy9VW3U= +cloud.google.com/go/dialogflow v1.69.0/go.mod h1:+2drAzrguQ8vltf6qn6foBPHrT/fFa1S3FQ40byV2WU= +cloud.google.com/go/dlp v1.24.0 h1:ThCQO8Qy5TAfFEJQjhq80u5c93UMdM2uqI3pUZVy7Do= +cloud.google.com/go/dlp v1.24.0/go.mod h1:y6EsWNgMDye72NtqjGHYZjN/wUDnO9CUygLV8iuFeW0= cloud.google.com/go/documentai v1.37.0 h1:7fla8GcarupO15eatRTUveXCob6DOSW1Wa+1i63CM3Q= cloud.google.com/go/documentai v1.37.0/go.mod h1:qAf3ewuIUJgvSHQmmUWvM3Ogsr5A16U2WPHmiJldvLA= -cloud.google.com/go/domains v0.10.3 h1:wnqN5YwMrtLSjn+HB2sChgmZ6iocOta4Q41giQsiRjY= -cloud.google.com/go/domains v0.10.3/go.mod h1:m7sLe18p0PQab56bVH3JATYOJqyRHhmbye6gz7isC7o= cloud.google.com/go/domains v0.10.6 h1:TI+Aavwc31KD8huOquJz0ISchCq1zSEWc9M+JcPJyxc= cloud.google.com/go/domains v0.10.6/go.mod h1:3xzG+hASKsVBA8dOPc4cIaoV3OdBHl1qgUpAvXK7pGY= -cloud.google.com/go/edgecontainer v1.4.1 h1:SwQuHQiheVfL7b5ar/AXDberiaqr/yiue8X55AdWnZU= -cloud.google.com/go/edgecontainer v1.4.1/go.mod h1:ubMQvXSxsvtEjJLyqcPFrdWrHfvjQxdoyt+SUrAi5ek= cloud.google.com/go/edgecontainer v1.4.3 h1:9tfGCicvrki927T+hGMB0yYmwIbRuZY6JR1/awrKiZ0= cloud.google.com/go/edgecontainer v1.4.3/go.mod h1:q9Ojw2ox0uhAvFisnfPRAXFTB1nfRIOIXVWzdXMZLcE= cloud.google.com/go/errorreporting v0.3.2 h1:isaoPwWX8kbAOea4qahcmttoS79+gQhvKsfg5L5AgH8= cloud.google.com/go/errorreporting v0.3.2/go.mod h1:s5kjs5r3l6A8UUyIsgvAhGq6tkqyBCUss0FRpsoVTww= -cloud.google.com/go/essentialcontacts v1.7.3 h1:Paw495vxVyKuAgcQ2NQk09iRZBhPYRytknydEnvzcv4= -cloud.google.com/go/essentialcontacts v1.7.3/go.mod h1:uimfZgDbhWNCmBpwUUPHe4vcMY2azsq/axC9f7vZFKI= cloud.google.com/go/essentialcontacts v1.7.6 h1:ysHZ4gr4plW1CL1Ur/AucUUfh20hDjSFbfjxSK0q/sk= cloud.google.com/go/essentialcontacts v1.7.6/go.mod h1:/Ycn2egr4+XfmAfxpLYsJeJlVf9MVnq9V7OMQr9R4lA= -cloud.google.com/go/eventarc v1.15.1 h1:RMymT7R87LaxKugOKwooOoheWXUm1NMeOfh3CVU9g54= -cloud.google.com/go/eventarc v1.15.1/go.mod h1:K2luolBpwaVOujZQyx6wdG4n2Xum4t0q1cMBmY1xVyI= cloud.google.com/go/eventarc v1.15.5 h1:bZW7ZMM+XXNErg6rOZcgxUzAgz4vpReRDP3ZiGf7/sI= cloud.google.com/go/eventarc v1.15.5/go.mod h1:vDCqGqyY7SRiickhEGt1Zhuj81Ya4F/NtwwL3OZNskg= -cloud.google.com/go/filestore v1.9.3 h1:vTXQI5qYKZ8dmCyHN+zVfaMyXCYbyZNM0CkPzpPUn7Q= -cloud.google.com/go/filestore v1.9.3/go.mod h1:Me0ZRT5JngT/aZPIKpIK6N4JGMzrFHRtGHd9ayUS4R4= cloud.google.com/go/filestore v1.10.2 h1:LjoAyp9TvVNBns3sUUzPaNsQiGpR2BReGmTS3bUCuBE= cloud.google.com/go/filestore v1.10.2/go.mod h1:w0Pr8uQeSRQfCPRsL0sYKW6NKyooRgixCkV9yyLykR4= cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= -cloud.google.com/go/functions v1.19.3 h1:V0vCHSgFTUqKn57+PUXp1UfQY0/aMkveAw7wXeM3Lq0= -cloud.google.com/go/functions v1.19.3/go.mod h1:nOZ34tGWMmwfiSJjoH/16+Ko5106x+1Iji29wzrBeOo= cloud.google.com/go/functions v1.19.6 h1:vJgWlvxtJG6p/JrbXAkz83DbgwOyFhZZI1Y32vUddjY= cloud.google.com/go/functions v1.19.6/go.mod h1:0G0RnIlbM4MJEycfbPZlCzSf2lPOjL7toLDwl+r0ZBw= cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc= -cloud.google.com/go/gkebackup v1.6.3 h1:djdExe/QgoKdp1gnIO1G5BoO1o/yGQOQJJEZ4QKTEXQ= -cloud.google.com/go/gkebackup v1.6.3/go.mod h1:JJzGsA8/suXpTDtqI7n9RZW97PXa2CIp+n8aRC/y57k= cloud.google.com/go/gkebackup v1.8.0 h1:eBqOt61yEChvj7I/GDPBbdCCRdUPudD1qrQYfYWV3Ok= cloud.google.com/go/gkebackup v1.8.0/go.mod h1:FjsjNldDilC9MWKEHExnK3kKJyTDaSdO1vF0QeWSOPU= -cloud.google.com/go/gkeconnect v0.12.1 h1:YVpR0vlHSP/wD74PXEbKua4Aamud+wiYm4TiewNjD3M= -cloud.google.com/go/gkeconnect v0.12.1/go.mod h1:L1dhGY8LjINmWfR30vneozonQKRSIi5DWGIHjOqo58A= cloud.google.com/go/gkeconnect v0.12.4 h1:67/rnPmF/I1Wmf7jWyKH+z4OWjU8ZUI0Vmzxvmzf3KY= cloud.google.com/go/gkeconnect v0.12.4/go.mod h1:bvpU9EbBpZnXGo3nqJ1pzbHWIfA9fYqgBMJ1VjxaZdk= -cloud.google.com/go/gkehub v0.15.3 h1:yZ6lNJ9rNIoQmWrG14dB3+BFjS/EIRBf7Bo6jc5QWlE= -cloud.google.com/go/gkehub v0.15.3/go.mod h1:nzFT/Q+4HdQES/F+FP1QACEEWR9Hd+Sh00qgiH636cU= cloud.google.com/go/gkehub v0.15.6 h1:9iogrmNNa+drDPf/zkLH/6KGgUf7FuuyokmithoGwMQ= cloud.google.com/go/gkehub v0.15.6/go.mod h1:sRT0cOPAgI1jUJrS3gzwdYCJ1NEzVVwmnMKEwrS2QaM= -cloud.google.com/go/gkemulticloud v1.5.1 h1:JWe6PDNpNU88ZYvQkTd7w28fgeIs/gg6i0hcjUkgZ3M= -cloud.google.com/go/gkemulticloud v1.5.1/go.mod h1:OdmhfSPXuJ0Kn9dQ2I3Ou7XZ3QK8caV4XVOJZwrIa3s= cloud.google.com/go/gkemulticloud v1.5.3 h1:334aZmOzIt3LVBpguCof8IHaLaftcZlx+L0TGBukYkY= cloud.google.com/go/gkemulticloud v1.5.3/go.mod h1:KPFf+/RcfvmuScqwS9/2MF5exZAmXSuoSLPuaQ98Xlk= cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM= -cloud.google.com/go/grafeas v0.3.11 h1:CobnwnyeY1j1Defi5vbEircI+jfrk3ci5m004ZjiFP4= -cloud.google.com/go/grafeas v0.3.11/go.mod h1:dcQyG2+T4tBgG0MvJAh7g2wl/xHV2w+RZIqivwuLjNg= -cloud.google.com/go/grafeas v0.3.15 h1:lBjwKmhpiqOAFaE0xdqF8CqO74a99s8tUT5mCkBBxPs= cloud.google.com/go/grafeas v0.3.15/go.mod h1:irwcwIQOBlLBotGdMwme8PipnloOPqILfIvMwlmu8Pk= -cloud.google.com/go/gsuiteaddons v1.7.4 h1:f3eMYsCDdg2AeldIPdKmBRxN1WoiTpE3RvX5orcm/I8= -cloud.google.com/go/gsuiteaddons v1.7.4/go.mod h1:gpE2RUok+HUhuK7RPE/fCOEgnTffS0lCHRaAZLxAMeE= cloud.google.com/go/gsuiteaddons v1.7.7 h1:sk0SxpCGIA7tIO//XdiiG29f2vrF6Pq/dsxxyBGiRBY= cloud.google.com/go/gsuiteaddons v1.7.7/go.mod h1:zTGmmKG/GEBCONsvMOY2ckDiEsq3FN+lzWGUiXccF9o= -cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= -cloud.google.com/go/iam v1.4.0/go.mod h1:gMBgqPaERlriaOV0CUl//XUzDhSfXevn4OEUbg6VRs4= -cloud.google.com/go/iap v1.10.3 h1:OWNYFHPyIBNHEAEFdVKOltYWe0g3izSrpFJW6Iidovk= -cloud.google.com/go/iap v1.10.3/go.mod h1:xKgn7bocMuCFYhzRizRWP635E2LNPnIXT7DW0TlyPJ8= cloud.google.com/go/iap v1.11.2 h1:VIioCrYsyWiRGx7Y8RDNylpI6d4t1Qx5ZgSLUVmWWPo= cloud.google.com/go/iap v1.11.2/go.mod h1:Bh99DMUpP5CitL9lK0BC8MYgjjYO4b3FbyhgW1VHJvg= -cloud.google.com/go/ids v1.5.3 h1:wbFF7twu0XScFr+dtsVxTTttbFIRYt/SJjZiHFidtYE= -cloud.google.com/go/ids v1.5.3/go.mod h1:a2MX8g18Eqs7yxD/pnEdid42SyBUm9LIzSWf8Jux9OY= cloud.google.com/go/ids v1.5.6 h1:uKGuaWozDcjg3wyf54Gd7tCH2YK8BFeH9qo1xBNiPKE= cloud.google.com/go/ids v1.5.6/go.mod h1:y3SGLmEf9KiwKsH7OHvYYVNIJAtXybqsD2z8gppsziQ= -cloud.google.com/go/iot v1.8.3 h1:aPWYQ+A1NX6ou/5U0nFAiXWdVT8OBxZYVZt2fBl2gWA= -cloud.google.com/go/iot v1.8.3/go.mod h1:dYhrZh+vUxIQ9m3uajyKRSW7moF/n0rYmA2PhYAkMFE= cloud.google.com/go/iot v1.8.6 h1:A3AhugnIViAZkC3/lHAQDaXBIk2ZOPBZS0XQCyZsjjc= cloud.google.com/go/iot v1.8.6/go.mod h1:MThnkiihNkMysWNeNje2Hp0GSOpEq2Wkb/DkBCVYa0U= -cloud.google.com/go/language v1.14.3 h1:8hmFMiS3wjjj3TX/U1zZYTgzwZoUjDbo9PaqcYEmuB4= -cloud.google.com/go/language v1.14.3/go.mod h1:hjamj+KH//QzF561ZuU2J+82DdMlFUjmiGVWpovGGSA= cloud.google.com/go/language v1.14.5 h1:BVJ/POtlnJ55LElvnQY19UOxpMVtHoHHkFJW2uHJsVU= cloud.google.com/go/language v1.14.5/go.mod h1:nl2cyAVjcBct1Hk73tzxuKebk0t2eULFCaruhetdZIA= -cloud.google.com/go/lifesciences v0.10.3 h1:Z05C+Ui953f0EQx9hJ1la6+QQl8ADrIs3iNwP5Elkpg= -cloud.google.com/go/lifesciences v0.10.3/go.mod h1:hnUUFht+KcZcliixAg+iOh88FUwAzDQQt5tWd7iIpNg= cloud.google.com/go/lifesciences v0.10.6 h1:Vu7XF4s5KJ8+mSLIL4eaQM6JTyWXvSB54oqC+CUZH20= cloud.google.com/go/lifesciences v0.10.6/go.mod h1:1nnZwaZcBThDujs9wXzECnd1S5d+UiDkPuJWAmhRi7Q= -cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= -cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= -cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= -cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= -cloud.google.com/go/managedidentities v1.7.3 h1:b9xGs24BIjfyvLgCtJoClOZpPi8d8owPgWe5JEINgaY= -cloud.google.com/go/managedidentities v1.7.3/go.mod h1:H9hO2aMkjlpY+CNnKWRh+WoQiUIDO8457wWzUGsdtLA= cloud.google.com/go/managedidentities v1.7.6 h1:zrZVWXZJlmHnfpyCrTQIbDBGUBHrcOOvrsjMjoXRxrk= cloud.google.com/go/managedidentities v1.7.6/go.mod h1:pYCWPaI1AvR8Q027Vtp+SFSM/VOVgbjBF4rxp1/z5p4= -cloud.google.com/go/maps v1.19.0 h1:deVm1ZFyCrUwxG11CdvtBz350VG5JUQ/LHTLnQrBgrM= -cloud.google.com/go/maps v1.19.0/go.mod h1:goHUXrmzoZvQjUVd0KGhH8t3AYRm17P8b+fsyR1UAmQ= -cloud.google.com/go/maps v1.21.0 h1:El61AfMxC1sU/RU8Wzs9dkZEgltyunKM86aKF9aDlaE= -cloud.google.com/go/maps v1.21.0/go.mod h1:cqzZ7+DWUKKbPTgqE+KuNQtiCRyg/o7WZF9zDQk+HQs= -cloud.google.com/go/mediatranslation v0.9.3 h1:nRBjeaMLipw05Br+qDAlSCcCQAAlat4mvpafztbEVgc= -cloud.google.com/go/mediatranslation v0.9.3/go.mod h1:KTrFV0dh7duYKDjmuzjM++2Wn6yw/I5sjZQVV5k3BAA= +cloud.google.com/go/maps v1.21.1 h1:IWnCNopmhrjwawsl2b/BdScu7IrGImSNaAD3LKttkxE= +cloud.google.com/go/maps v1.21.1/go.mod h1:TAt/cYHndJQBrir8DN8OHiS0HvKwsBTqDGRfAtLIulU= cloud.google.com/go/mediatranslation v0.9.6 h1:SDGatA73TgZ8iCvILVXpk/1qhTK5DJyufUDEWgbmbV8= cloud.google.com/go/mediatranslation v0.9.6/go.mod h1:WS3QmObhRtr2Xu5laJBQSsjnWFPPthsyetlOyT9fJvE= -cloud.google.com/go/memcache v1.11.3 h1:XH/qT3GbbSH//R0JTqR77lRpBxaa0N9sHgAzfwbTrv0= -cloud.google.com/go/memcache v1.11.3/go.mod h1:UeWI9cmY7hvjU1EU6dwJcQb6EFG4GaM3KNXOO2OFsbI= cloud.google.com/go/memcache v1.11.6 h1:33IVqQEmFiITsBXwGHeTkUhWz0kLNKr90nV3e22uLPs= cloud.google.com/go/memcache v1.11.6/go.mod h1:ZM6xr1mw3F8TWO+In7eq9rKlJc3jlX2MDt4+4H+/+cc= -cloud.google.com/go/metastore v1.14.3 h1:jDqeCw6NGDRAPT9+2Y/EjnWAB0BfCcUfmPLOyhB0eHs= -cloud.google.com/go/metastore v1.14.3/go.mod h1:HlbGVOvg0ubBLVFRk3Otj3gtuzInuzO/TImOBwsKlG4= cloud.google.com/go/metastore v1.14.7 h1:dLm59AHHZCorveCylj7c2iWhkQsmMIeWTsV+tG/BXtY= cloud.google.com/go/metastore v1.14.7/go.mod h1:0dka99KQofeUgdfu+K/Jk1KeT9veWZlxuZdJpZPtuYU= -cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= -cloud.google.com/go/monitoring v1.22.1/go.mod h1:AuZZXAoN0WWWfsSvET1Cpc4/1D8LXq8KRDU87fMS6XY= -cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= -cloud.google.com/go/networkconnectivity v1.16.1 h1:YsVhG71ZC4FkqCP2oCI55x/JeGFyd7738Lt8iNTrzJw= -cloud.google.com/go/networkconnectivity v1.16.1/go.mod h1:GBC1iOLkblcnhcnfRV92j4KzqGBrEI6tT7LP52nZCTk= cloud.google.com/go/networkconnectivity v1.17.1 h1:RQcG1rZNCNV5Dn3tnINs4TYswDXk2hKH+85eh+JvoWU= cloud.google.com/go/networkconnectivity v1.17.1/go.mod h1:DTZCq8POTkHgAlOAAEDQF3cMEr/B9k1ZbpklqvHEBtg= -cloud.google.com/go/networkmanagement v1.18.0 h1:oEoFGPYxTBsY47h0zdoE2ojV5aU/541D83UmxfjHWaE= -cloud.google.com/go/networkmanagement v1.18.0/go.mod h1:yTxpAFuvQOOKgL3W7+k2Rp1bSKTxyRcZ5xNHGdHUM6w= cloud.google.com/go/networkmanagement v1.19.1 h1:ecukgArkYCVcK5w2h7WDDd+nHgmBAp9Bst7ClmVKz5A= cloud.google.com/go/networkmanagement v1.19.1/go.mod h1:icgk265dNnilxQzpr6rO9WuAuuCmUOqq9H6WBeM2Af4= -cloud.google.com/go/networksecurity v0.10.3 h1:JLJBFbxc8D7/OS81MyRoKhc2OvnVJxy5VMoQqqAhA7k= -cloud.google.com/go/networksecurity v0.10.3/go.mod h1:G85ABVcPscEgpw+gcu+HUxNZJWjn3yhTqEU7+SsltFM= cloud.google.com/go/networksecurity v0.10.6 h1:6b6fcCG9BFNcmtNO+VuPE04vkZb5TKNX9+7ZhYMgstE= cloud.google.com/go/networksecurity v0.10.6/go.mod h1:FTZvabFPvK2kR/MRIH3l/OoQ/i53eSix2KA1vhBMJec= -cloud.google.com/go/notebooks v1.12.3 h1:+9DrGJcZhCu6B2t0JJorekjIUBvg/KvBmXJYGmfvVvA= -cloud.google.com/go/notebooks v1.12.3/go.mod h1:I0pMxZct+8Rega2LYrXL8jGAGZgLchSmh8Ksc+0xNyA= cloud.google.com/go/notebooks v1.12.6 h1:nCfZwVihArMPP2atRoxRrXOXJ/aC9rAgpBQGCc2zpYw= cloud.google.com/go/notebooks v1.12.6/go.mod h1:3Z4TMEqAKP3pu6DI/U+aEXrNJw9hGZIVbp+l3zw8EuA= -cloud.google.com/go/optimization v1.7.3 h1:JwQjjoBZJpsoMQe/3mhVBMVZuSdagHg2pGOnwh2Jk+E= -cloud.google.com/go/optimization v1.7.3/go.mod h1:GlYFp4Mju0ybK5FlOUtV6zvWC00TIScdbsPyF6Iv144= cloud.google.com/go/optimization v1.7.6 h1:jDvIuSxDsXI2P7l2sYXm6CoX1YBIIT6Khm5m0hq0/KQ= cloud.google.com/go/optimization v1.7.6/go.mod h1:4MeQslrSJGv+FY4rg0hnZBR/tBX2awJ1gXYp6jZpsYY= -cloud.google.com/go/orchestration v1.11.4 h1:SFAsKyqvtS8VFcsq+JgXAeRkrksB9UH+AH7iFamkmlc= -cloud.google.com/go/orchestration v1.11.4/go.mod h1:UKR2JwogaZmDGnAcBgAQgCPn89QMqhXFUCYVhHd31vs= cloud.google.com/go/orchestration v1.11.9 h1:PnlZ/O4R/eiounpxUkhI9ZXRMWbG7vFqxc6L6sR+31k= cloud.google.com/go/orchestration v1.11.9/go.mod h1:KKXK67ROQaPt7AxUS1V/iK0Gs8yabn3bzJ1cLHw4XBg= -cloud.google.com/go/orgpolicy v1.14.2 h1:WFvgmjq/FO5GiXlhebltA9N14KdbLMcgG88ME+SWeBo= -cloud.google.com/go/orgpolicy v1.14.2/go.mod h1:2fTDMT3X048iFKxc6DEgkG+a/gN+68qEgtPrHItKMzo= cloud.google.com/go/orgpolicy v1.15.0 h1:uQziDu3UKYk9ZwUgneZAW5aWxZFKgOXXsuVKFKh0z7Y= cloud.google.com/go/orgpolicy v1.15.0/go.mod h1:NTQLwgS8N5cJtdfK55tAnMGtvPSsy95JJhESwYHaJVs= -cloud.google.com/go/osconfig v1.14.3 h1:cyf1PMK5c2/WOIr5r2lxjH/XBJMA9P4zC8Tm10i0z3M= -cloud.google.com/go/osconfig v1.14.3/go.mod h1:9D2MS1Etne18r/mAeW5jtto3toc9H1qu9wLNDG3NvQg= cloud.google.com/go/osconfig v1.14.6 h1:4uJrA1obzMBp1I+DF15y/MvsXKIODevuANpq3QhvX30= cloud.google.com/go/osconfig v1.14.6/go.mod h1:LS39HDBH0IJDFgOUkhSZUHFQzmcWaCpYXLrc3A4CVzI= -cloud.google.com/go/oslogin v1.14.3 h1:yomxnFPk+ye0zd0mJ15nn9fH4Ns7ex4xA3ll+u2q59A= -cloud.google.com/go/oslogin v1.14.3/go.mod h1:fDEGODTG/W9ZGUTHTlMh8euXWC1fTcgjJ9Kcxxy14a8= cloud.google.com/go/oslogin v1.14.6 h1:BDKVcxo1OO4ZT+PbuFchZjnbrlUGfChilt6+pITY1VI= cloud.google.com/go/oslogin v1.14.6/go.mod h1:xEvcRZTkMXHfNSKdZ8adxD6wvRzeyAq3cQX3F3kbMRw= -cloud.google.com/go/phishingprotection v0.9.3 h1:T5mGFV0ggBKg3qt9myFRiGJu+nIUucuHLAtVpAuQ08I= -cloud.google.com/go/phishingprotection v0.9.3/go.mod h1:ylzN9HruB/X7dD50I4sk+FfYzuPx9fm5JWsYI0t7ncc= cloud.google.com/go/phishingprotection v0.9.6 h1:yl572bBQbPjflX250SOflN6gwO2uYoddN2uRp36fDTo= cloud.google.com/go/phishingprotection v0.9.6/go.mod h1:VmuGg03DCI0wRp/FLSvNyjFj+J8V7+uITgHjCD/x4RQ= -cloud.google.com/go/policytroubleshooter v1.11.3 h1:ekIWI8JbKkpOfrgH/THGamQE/D16tcVBYJyrkseVcYI= -cloud.google.com/go/policytroubleshooter v1.11.3/go.mod h1:AFHlORqh4AnMC0twc2yPKfzlozp3DO0yo9OfOd9aNOs= cloud.google.com/go/policytroubleshooter v1.11.6 h1:Z8+tO2z21MY1arBBuJjwrOjbw8fbZb13AZTHXdzkl2U= cloud.google.com/go/policytroubleshooter v1.11.6/go.mod h1:jdjYGIveoYolk38Dm2JjS5mPkn8IjVqPsDHccTMu3mY= -cloud.google.com/go/privatecatalog v0.10.4 h1:fu2LABMi7CgZORQ2oNGbc0hoZ0FTqLkjGqIgAV/Kc7U= -cloud.google.com/go/privatecatalog v0.10.4/go.mod h1:n/vXBT+Wq8B4nSRUJNDsmqla5BYjbVxOlHzS6PjiF+w= cloud.google.com/go/privatecatalog v0.10.7 h1:R951ikhxIanXEijBCu0xnoUAOteS5m/Xplek0YvsNTE= cloud.google.com/go/privatecatalog v0.10.7/go.mod h1:Fo/PF/B6m4A9vUYt0nEF1xd0U6Kk19/Je3eZGrQ6l60= -cloud.google.com/go/pubsub v1.47.0 h1:Ou2Qu4INnf7ykrFjGv2ntFOjVo8Nloh/+OffF4mUu9w= -cloud.google.com/go/pubsub v1.47.0/go.mod h1:LaENesmga+2u0nDtLkIOILskxsfvn/BXX9Ak1NFxOs8= cloud.google.com/go/pubsub v1.49.0 h1:5054IkbslnrMCgA2MAEPcsN3Ky+AyMpEZcii/DoySPo= cloud.google.com/go/pubsub v1.49.0/go.mod h1:K1FswTWP+C1tI/nfi3HQecoVeFvL4HUOB1tdaNXKhUY= cloud.google.com/go/pubsublite v1.8.2 h1:jLQozsEVr+c6tOU13vDugtnaBSUy/PD5zK6mhm+uF1Y= cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= -cloud.google.com/go/recaptchaenterprise/v2 v2.19.4 h1:T5YGzaXwTesHaPDNTAuU3neDwZEnfjce70zufPFUwno= -cloud.google.com/go/recaptchaenterprise/v2 v2.19.4/go.mod h1:WaglfocMJGkqZVdXY/FVB7OhoVRONPS4uXqtNn6HfX0= cloud.google.com/go/recaptchaenterprise/v2 v2.20.4 h1:P4QMryKcWdi4LIe1Sx0b2ZOAQv5gVfdzPt2peXcN32Y= cloud.google.com/go/recaptchaenterprise/v2 v2.20.4/go.mod h1:3H8nb8j8N7Ss2eJ+zr+/H7gyorfzcxiDEtVBDvDjwDQ= -cloud.google.com/go/recommendationengine v0.9.3 h1:kBpcYPx4ys4lrDGKp4OhP2uy8h7UjlmLW/qoO5Xb2bY= -cloud.google.com/go/recommendationengine v0.9.3/go.mod h1:QRnX5aM7DCvtqtSs7I0zay5Zfq3fzxqnsPbZF7pa1G8= cloud.google.com/go/recommendationengine v0.9.6 h1:slN7h23vswGccW8x3f+xUXCu9Yo18/GNkazH93LJbFk= cloud.google.com/go/recommendationengine v0.9.6/go.mod h1:nZnjKJu1vvoxbmuRvLB5NwGuh6cDMMQdOLXTnkukUOE= -cloud.google.com/go/recommender v1.13.3 h1:dVlOjxsbjuhlwu4MIcyPWe09qVcDqc419iOjdPl5RHk= -cloud.google.com/go/recommender v1.13.3/go.mod h1:6yAmcfqJRKglZrVuTHsieTFEm4ai9JtY3nQzmX4TC0Q= cloud.google.com/go/recommender v1.13.5 h1:cIsyRKGNw4LpCfY5c8CCQadhlp54jP4fHtP+d5Sy2xE= cloud.google.com/go/recommender v1.13.5/go.mod h1:v7x/fzk38oC62TsN5Qkdpn0eoMBh610UgArJtDIgH/E= -cloud.google.com/go/redis v1.18.0 h1:xcu35SCyHSp+nKV6QNIklgkBKTH1qb0aLUXjl0mSR8I= -cloud.google.com/go/redis v1.18.0/go.mod h1:fJ8dEQJQ7DY+mJRMkSafxQCuc8nOyPUwo9tXJqjvNEY= cloud.google.com/go/redis v1.18.2 h1:JlHLceAOILEmbn+NIS7l+vmUKkFuobLToCWTxL7NGcQ= cloud.google.com/go/redis v1.18.2/go.mod h1:q6mPRhLiR2uLf584Lcl4tsiRn0xiFlu6fnJLwCORMtY= -cloud.google.com/go/resourcemanager v1.10.3 h1:SHOMw0kX0xWratC5Vb5VULBeWiGlPYAs82kiZqNtWpM= -cloud.google.com/go/resourcemanager v1.10.3/go.mod h1:JSQDy1JA3K7wtaFH23FBGld4dMtzqCoOpwY55XYR8gs= cloud.google.com/go/resourcemanager v1.10.6 h1:LIa8kKE8HF71zm976oHMqpWFiaDHVw/H1YMO71lrGmo= cloud.google.com/go/resourcemanager v1.10.6/go.mod h1:VqMoDQ03W4yZmxzLPrB+RuAoVkHDS5tFUUQUhOtnRTg= cloud.google.com/go/resourcesettings v1.8.3 h1:13HOFU7v4cEvIHXSAQbinF4wp2Baybbq7q9FMctg1Ek= cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= -cloud.google.com/go/retail v1.19.2 h1:PT6CUlazIFIOLLJnV+bPBtiSH8iusKZ+FZRzZYFt2vk= -cloud.google.com/go/retail v1.19.2/go.mod h1:71tRFYAcR4MhrZ1YZzaJxr030LvaZiIcupH7bXfFBcY= -cloud.google.com/go/retail v1.21.0 h1:8jgWgtAg1mk91WmaoWRTlL9CcvazPwqZ3YT9n6Gva9U= -cloud.google.com/go/retail v1.21.0/go.mod h1:LuG+QvBdLfKfO+7nnF3eA3l1j4TQw3Sg+UqlUorquRc= -cloud.google.com/go/run v1.9.0 h1:9WeTqeEcriXqRViXMNwczjFJjixOSBlSlk/fW3lfKPg= -cloud.google.com/go/run v1.9.0/go.mod h1:Dh0+mizUbtBOpPEzeXMM22t8qYQpyWpfmUiWQ0+94DU= -cloud.google.com/go/run v1.10.0 h1:CDhz0PPzI/cVpmNFyHe3Yp21jNpiAqtkfRxuoLi+JU0= -cloud.google.com/go/run v1.10.0/go.mod h1:z7/ZidaHOCjdn5dV0eojRbD+p8RczMk3A7Qi2L+koHg= -cloud.google.com/go/scheduler v1.11.4 h1:ewVvigBnEnrr9Ih8CKnLVoB5IiULaWfYU5nEnnfVAto= -cloud.google.com/go/scheduler v1.11.4/go.mod h1:0ylvH3syJnRi8EDVo9ETHW/vzpITR/b+XNnoF+GPSz4= +cloud.google.com/go/retail v1.22.0 h1:4Nvso8WG3J2T8F78rs61g1INwpd3feX8WPI/gS71w8s= +cloud.google.com/go/retail v1.22.0/go.mod h1:INfBxkiT1UcK+ohGFP9hsCM2UVECK3EzjfCut4lgDf8= +cloud.google.com/go/run v1.10.1 h1:MbP5yejLol5IagqF5TYnU7JNXR5UhzCqPZ9OMZXr5c0= +cloud.google.com/go/run v1.10.1/go.mod h1:aQWMoB2SNXQJUYIOeLRCacO/2VwdSO9FxpM6jxrimAo= cloud.google.com/go/scheduler v1.11.7 h1:zkMEJ0UbEJ3O7NwEUlKLIp6eXYv1L7wHjbxyxznajKM= cloud.google.com/go/scheduler v1.11.7/go.mod h1:gqYs8ndLx2M5D0oMJh48aGS630YYvC432tHCnVWN13s= -cloud.google.com/go/secretmanager v1.14.5 h1:W++V0EL9iL6T2+ec24Dm++bIti0tI6Gx6sCosDBters= -cloud.google.com/go/secretmanager v1.14.5/go.mod h1:GXznZF3qqPZDGZQqETZwZqHw4R6KCaYVvcGiRBA+aqY= -cloud.google.com/go/secretmanager v1.14.7 h1:VkscIRzj7GcmZyO4z9y1EH7Xf81PcoiAo7MtlD+0O80= -cloud.google.com/go/secretmanager v1.14.7/go.mod h1:uRuB4F6NTFbg0vLQ6HsT7PSsfbY7FqHbtJP1J94qxGc= -cloud.google.com/go/security v1.18.3 h1:ya9gfY1ign6Yy25VMMMgZ9xy7D/TczDB0ElXcyWmEVE= -cloud.google.com/go/security v1.18.3/go.mod h1:NmlSnEe7vzenMRoTLehUwa/ZTZHDQE59IPRevHcpCe4= -cloud.google.com/go/security v1.18.5 h1:6hqzvuwC8za9jyCTxygmEHnp4vZ8hfhwKVArxSCAVCo= -cloud.google.com/go/security v1.18.5/go.mod h1:D1wuUkDwGqTKD0Nv7d4Fn2Dc53POJSmO4tlg1K1iS7s= -cloud.google.com/go/securitycenter v1.36.0 h1:IdDiAa7gYtL7Gdx+wEaNHimudk3ZkEGNhdz9FuEuxWM= -cloud.google.com/go/securitycenter v1.36.0/go.mod h1:AErAQqIvrSrk8cpiItJG1+ATl7SD7vQ6lgTFy/Tcs4Q= -cloud.google.com/go/securitycenter v1.36.2 h1:hLA58IBYmWrNiXDIONvuCUQ4sHLVPy8JvDo2j1wSYCw= -cloud.google.com/go/securitycenter v1.36.2/go.mod h1:80ocoXS4SNWxmpqeEPhttYrmlQzCPVGaPzL3wVcoJvE= +cloud.google.com/go/secretmanager v1.15.0 h1:RtkCMgTpaBMbzozcRUGfZe46jb9a3qh5EdEtVRUATF8= +cloud.google.com/go/secretmanager v1.15.0/go.mod h1:1hQSAhKK7FldiYw//wbR/XPfPc08eQ81oBsnRUHEvUc= +cloud.google.com/go/security v1.19.0 h1:GI6kufPbHFINq998M7x60rfV5MVo6yhD+uVrjNzueKw= +cloud.google.com/go/security v1.19.0/go.mod h1:ks6NsA9Q6UODfLLgXr4MrxC/p7Bc5k15zqcfwvqlIlw= +cloud.google.com/go/securitycenter v1.37.0 h1:UR8cUgXFYpWxKkKnUNy65hlrAzgwBZBVxZilJ50ESXU= +cloud.google.com/go/securitycenter v1.37.0/go.mod h1:DdQi6OEzw1rmLtPpqtUx6bqnQq8ZdCVuG9eZRYz2QAE= cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= -cloud.google.com/go/servicedirectory v1.12.3 h1:oFkCp6ti7fc7hzeROmOPQuPBHFqwyhcsv3Yrma28+uc= -cloud.google.com/go/servicedirectory v1.12.3/go.mod h1:dwTKSCYRD6IZMrqoBCIvZek+aOYK/6+jBzOGw8ks5aY= cloud.google.com/go/servicedirectory v1.12.6 h1:pl/KUNvFzlXpxgnPgzQjyTQQcv5WsQ97zCHaPrLQlYA= cloud.google.com/go/servicedirectory v1.12.6/go.mod h1:OojC1KhOMDYC45oyTn3Mup08FY/S0Kj7I58dxUMMTpg= cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= -cloud.google.com/go/shell v1.8.3 h1:mjYgUsOtV3jl9xvDmcvlRRmA64deEPf52zOfuc68b/g= -cloud.google.com/go/shell v1.8.3/go.mod h1:OYcrgWF6JSp/uk76sNTtYFlMD0ho2+Cdzc7U3P/bF54= cloud.google.com/go/shell v1.8.6 h1:jLWyztGlNWBx55QXBM4HbWvfv7aiRjPzRKTUkZA8dXk= cloud.google.com/go/shell v1.8.6/go.mod h1:GNbTWf1QA/eEtYa+kWSr+ef/XTCDkUzRpV3JPw0LqSk= -cloud.google.com/go/spanner v1.82.0 h1:w9uO8RqEoBooBLX4nqV1RtgudyU2ZX780KTLRgeVg60= -cloud.google.com/go/spanner v1.82.0/go.mod h1:BzybQHFQ/NqGxvE/M+/iU29xgutJf7Q85/4U9RWMto0= -cloud.google.com/go/speech v1.26.0 h1:qvURtJs7BQzQhbxWxwai0pT79S8KLVKJ/4W8igVkt1Y= -cloud.google.com/go/speech v1.26.0/go.mod h1:78bqDV2SgwFlP/M4n3i3PwLthFq6ta7qmyG6lUV7UCA= -cloud.google.com/go/speech v1.27.1 h1:+OktATNlQc+4WH78OrQadIP4CzXb9mBucdDGCO1NrlI= -cloud.google.com/go/speech v1.27.1/go.mod h1:efCfklHFL4Flxcdt9gpEMEJh9MupaBzw3QiSOVeJ6ck= -cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU= -cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= -cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA= -cloud.google.com/go/storagetransfer v1.12.1 h1:W3v9A7MGBN7H9sAFstyciwP/1XEQhUhZfrjclmDnpMs= -cloud.google.com/go/storagetransfer v1.12.1/go.mod h1:hQqbfs8/LTmObJyCC0KrlBw8yBJ2bSFlaGila0qBMk4= +cloud.google.com/go/spanner v1.83.0 h1:AH3QIoSIa01l3WbeTppkwCEYFNK1AER6drcYhPmwhxY= +cloud.google.com/go/spanner v1.83.0/go.mod h1:QSWcjxszT0WRHNd8zyGI0WctrYA1N7j0yTFsWyol9Yw= +cloud.google.com/go/speech v1.28.0 h1:9AuiAxDTmh/aeREtw+/0e7aI27T5QN4fK5lhssc9MxA= +cloud.google.com/go/speech v1.28.0/go.mod h1:hJf6oa+1rzCW/CeDE/qCXedV20B2TXEUje5iaGwW+JI= cloud.google.com/go/storagetransfer v1.13.0 h1:uqKX3OgcYzR1W1YI943ZZ45id0RqA2eXXoCBSPstlbw= cloud.google.com/go/storagetransfer v1.13.0/go.mod h1:+aov7guRxXBYgR3WCqedkyibbTICdQOiXOdpPcJCKl8= -cloud.google.com/go/talent v1.8.0 h1:olv+s2g+LGXeJi+MYF1wI44/TwHaVnO0N7PiucVf5ZQ= -cloud.google.com/go/talent v1.8.0/go.mod h1:/gvOzSrtMcfTL/9xWhdYaZATaxUNhQ+L+3ZaGOGs7bA= cloud.google.com/go/talent v1.8.3 h1:wDP+++O/P1cTJBMkYlSY46k0a6atSoyO+UkBGuU9+Ao= cloud.google.com/go/talent v1.8.3/go.mod h1:oD3/BilJpJX8/ad8ZUAxlXHCslTg2YBbafFH3ciZSLQ= -cloud.google.com/go/texttospeech v1.11.0 h1:YF/RdNb+jUEp22cIZCvqiFjfA5OxGE+Dxss3mhXU7oQ= -cloud.google.com/go/texttospeech v1.11.0/go.mod h1:7M2ro3I2QfIEvArFk1TJ+pqXJqhszDtxUpnIv/150As= cloud.google.com/go/texttospeech v1.13.0 h1:oWWFQp0yFl4EJOr3opDkKH9304wUsZjgPjrTDS6S1a8= cloud.google.com/go/texttospeech v1.13.0/go.mod h1:g/tW/m0VJnulGncDrAoad6WdELMTes8eb77Idz+4HCo= -cloud.google.com/go/tpu v1.8.0 h1:BvMNijOb6Vd46Rr/SR5jWv1MPosOhVsi0UaeAGNjeds= -cloud.google.com/go/tpu v1.8.0/go.mod h1:XyNzyK1xc55WvL5rZEML0Z9/TUHDfnq0uICkQw6rWMo= cloud.google.com/go/tpu v1.8.3 h1:S4Ptq+yFIPNLEzQ/OQwiIYDNzk5I2vYmhf0SmFQOmWo= cloud.google.com/go/tpu v1.8.3/go.mod h1:Do6Gq+/Jx6Xs3LcY2WhHyGwKDKVw++9jIJp+X+0rxRE= -cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= -cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs= -cloud.google.com/go/translate v1.12.3 h1:XJ7LipYJi80BCgVk2lx1fwc7DIYM6oV2qx1G4IAGQ5w= -cloud.google.com/go/translate v1.12.3/go.mod h1:qINOVpgmgBnY4YTFHdfVO4nLrSBlpvlIyosqpGEgyEg= -cloud.google.com/go/translate v1.12.5 h1:QPMNi4WCtHwc2PPfxbyUMwdN/0+cyCGLaKi2tig41J8= -cloud.google.com/go/translate v1.12.5/go.mod h1:o/v+QG/bdtBV1d1edmtau0PwTfActvxPk/gtqdSDBi4= -cloud.google.com/go/video v1.23.3 h1:C2FH+6yr6LCZC4fP0gm9FwJB/SRh5Ul88O5Sc/bL83I= -cloud.google.com/go/video v1.23.3/go.mod h1:Kvh/BheubZxGZDXSb0iO6YX7ZNcaYHbLjnnaC8Qyy3g= +cloud.google.com/go/translate v1.12.6 h1:QHcszWZvBLEZHM2WJ6IDg2BUTWzEPMiHhbJAd15yKGU= +cloud.google.com/go/translate v1.12.6/go.mod h1:nB3AXuX+iHbV8ZURmElcW85qkEDWZw68sf4kqMT/E5o= cloud.google.com/go/video v1.24.0 h1:KTB2BEXjGm2K/JcKxQXEgx3nSoMTByepnPZa4kln064= cloud.google.com/go/video v1.24.0/go.mod h1:h6Bw4yUbGNEa9dH4qMtUMnj6cEf+OyOv/f2tb70G6Fk= -cloud.google.com/go/videointelligence v1.12.3 h1:zNTOUQyatGQtnCJ2dR3faRtpWQOlC8wszJqwG5CtwVM= -cloud.google.com/go/videointelligence v1.12.3/go.mod h1:dUA6V+NH7CVgX6TePq0IelVeBMGzvehxKPR4FGf1dtw= cloud.google.com/go/videointelligence v1.12.6 h1:heq7jEO39sH5TycBh8TGFJ827XCxK0tIWatmBY/n0jI= cloud.google.com/go/videointelligence v1.12.6/go.mod h1:/l34WMndN5/bt04lHodxiYchLVuWPQjCU6SaiTswrIw= cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= -cloud.google.com/go/vision/v2 v2.9.3 h1:dPvfDuPqPH+Yscf0f2f1RprvKkoo+N/j0a+IbLYX7Cs= -cloud.google.com/go/vision/v2 v2.9.3/go.mod h1:weAcT8aNYSgrWWVTC2PuJTc7fcXKvUeAyDq8B6HkLSg= cloud.google.com/go/vision/v2 v2.9.5 h1:UJZ0H6UlOaYKgCn6lWG2iMAOJIsJZLnseEfzBR8yIqQ= cloud.google.com/go/vision/v2 v2.9.5/go.mod h1:1SiNZPpypqZDbOzU052ZYRiyKjwOcyqgGgqQCI/nlx8= -cloud.google.com/go/vmmigration v1.8.3 h1:dpCQq3pj2HnKdbvGTftdWymm3r4ovF7JW5z8xBcO2x4= -cloud.google.com/go/vmmigration v1.8.3/go.mod h1:8CzUpK9eBzohgpL4RvBVtW4sY/sDliVyQonTFQfWcJ4= cloud.google.com/go/vmmigration v1.8.6 h1:68hOQDhs1DOITrCrhritrwr8xy6s8QMdwDyMzMiFleU= cloud.google.com/go/vmmigration v1.8.6/go.mod h1:uZ6/KXmekwK3JmC8PzBM/cKQmq404TTfWtThF6bbf0U= -cloud.google.com/go/vmwareengine v1.3.3 h1:TfuQr5j7qriINulUMotaC/+27SQaW2thIkF3Gb6VJ38= -cloud.google.com/go/vmwareengine v1.3.3/go.mod h1:G7vz05KGijha0c0dj1INRKyDAaQW8TRMZt/FrfOZVXc= cloud.google.com/go/vmwareengine v1.3.5 h1:OsGd1SB91y9fDuzdzFngMv4UcT4cqmRxjsCsS4Xmcu8= cloud.google.com/go/vmwareengine v1.3.5/go.mod h1:QuVu2/b/eo8zcIkxBYY5QSwiyEcAy6dInI7N+keI+Jg= -cloud.google.com/go/vpcaccess v1.8.3 h1:vxVaoFM64M/ht619c4wZNF0iq0QPaMWElOh7Ns4r41A= -cloud.google.com/go/vpcaccess v1.8.3/go.mod h1:bqOhyeSh/nEmLIsIUoCiQCBHeNPNjaK9M3bIvKxFdsY= cloud.google.com/go/vpcaccess v1.8.6 h1:RYtUB9rQEijX9Tc6lQcGst58ZOzPgaYTkz6+2pyPQTM= cloud.google.com/go/vpcaccess v1.8.6/go.mod h1:61yymNplV1hAbo8+kBOFO7Vs+4ZHYI244rSFgmsHC6E= -cloud.google.com/go/webrisk v1.10.3 h1:yh0v/5n49VO4/i9pYfDm1gLJUj1Ph3Xzegn8WvK9YRA= -cloud.google.com/go/webrisk v1.10.3/go.mod h1:rRAqCA5/EQOX8ZEEF4HMIrLHGTK/Y1hEQgWMnih+jAw= cloud.google.com/go/webrisk v1.11.1 h1:yZKNB7zRxOMriLrhP5WDE+BjxXVl0wJHHZSdaYzbdVU= cloud.google.com/go/webrisk v1.11.1/go.mod h1:+9SaepGg2lcp1p0pXuHyz3R2Yi2fHKKb4c1Q9y0qbtA= -cloud.google.com/go/websecurityscanner v1.7.3 h1:/uxhVCWKXzPw5pVfnBOVjaSiQ6Bm0tDExDOCLV40thw= -cloud.google.com/go/websecurityscanner v1.7.3/go.mod h1:gy0Kmct4GNLoCePWs9xkQym1D7D59ld5AjhXrjipxSs= cloud.google.com/go/websecurityscanner v1.7.6 h1:cIPKJKZA3l7D8DfL4nxce8HGOWXBw3WAUBF0ymOW9GQ= cloud.google.com/go/websecurityscanner v1.7.6/go.mod h1:ucaaTO5JESFn5f2pjdX01wGbQ8D6h79KHrmO2uGZeiY= -cloud.google.com/go/workflows v1.13.3 h1:lNFDMranJymDEB7cTI7DI9czbc1WU0RWY9KCEv9zuDY= -cloud.google.com/go/workflows v1.13.3/go.mod h1:Xi7wggEt/ljoEcyk+CB/Oa1AHBCk0T1f5UH/exBB5CE= cloud.google.com/go/workflows v1.14.2 h1:phBz5TOAES0YGogxZ6Q7ISSudaf618lRhE3euzBpE9U= cloud.google.com/go/workflows v1.14.2/go.mod h1:5nqKjMD+MsJs41sJhdVrETgvD5cOK3hUcAs8ygqYvXQ= codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0= @@ -496,60 +255,44 @@ codeberg.org/go-latex/latex v0.1.0 h1:hoGO86rIbWVyjtlDLzCqZPjNykpWQ9YuTZqAzPcfL3 codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= codeberg.org/go-pdf/fpdf v0.10.0 h1:u+w669foDDx5Ds43mpiiayp40Ov6sZalgcPMDBcZRd4= codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= -contrib.go.opencensus.io/exporter/aws v0.0.0-20230502192102-15967c811cec h1:CSNP8nIEQt4sZEo2sGUiWSmVJ9c5QdyIQvwzZAsn+8Y= -contrib.go.opencensus.io/exporter/aws v0.0.0-20230502192102-15967c811cec/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/ocagent v0.6.0 h1:Z1n6UAyr0QwM284yUuh5Zd8JlvxUGAhFZcgMJkMPrGM= -contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d h1:LblfooH1lKOpp1hIhukktmSAxFkqMPFk9KR6iZ0MJNI= -contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d/go.mod h1:IshRmMJBhDfFj5Y67nVhMYTTIze91RUeT73ipWKs/GY= -contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= -contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= -contrib.go.opencensus.io/exporter/stackdriver v0.13.14 h1:zBakwHardp9Jcb8sQHcHpXy/0+JIb1M8KjigCJzx7+4= -contrib.go.opencensus.io/exporter/stackdriver v0.13.14/go.mod h1:5pSSGY0Bhuk7waTHuDf4aQ8D2DrhgETRo9fy6k3Xlzc= -contrib.go.opencensus.io/exporter/zipkin v0.1.2 h1:YqE293IZrKtqPnpwDPH/lOqTWD/s3Iwabycam74JV3g= -contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj3gsxcuytSqtH0dxSWW1RE= -contrib.go.opencensus.io/integrations/ocsql v0.1.7 h1:G3k7C0/W44zcqkpRSFyjU9f6HZkbwIrL//qqnlqWZ60= -contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= +contrib.go.opencensus.io/exporter/prometheus v0.4.0/go.mod h1:o7cosnyfuPVK0tB8q0QmaQNhGnptITnPQB+z1+qeFB0= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38= git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= -github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng= -github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk= github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= -github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1 h1:o/Ws6bEqMeKZUfj1RRm3mQ51O8JGU5w+Qdg2AhHib6A= -github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1/go.mod h1:6QAMYBAbQeeKX+REFJMZ1nFWu9XLw/PPcjYpuc9RDFs= -github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 h1:JNgM3Tz592fUHU2vgwgvOgKxo5s9Ki0y2wicBeckn70= -github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0/go.mod h1:6vUKmzY17h6dpn9ZLAhM4R/rcrltBeq52qZIkUR7Oro= -github.com/Azure/go-amqp v1.0.5 h1:po5+ljlcNSU8xtapHTe8gIc8yHxCzC03E8afH2g1ftU= -github.com/Azure/go-amqp v1.0.5/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 h1:CRZwf68N55u7ZZo3Xx2ynuqEA6k5GZfwsEUkU8qsAPk= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1/go.mod h1:NydgUaroiShkgOcb+X6OUdS3RalWBrvDNtOyFHJtsZY= +github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4= github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= -github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU= -github.com/ClickHouse/ch-go v0.65.1/go.mod h1:bsodgURwmrkvkBe5jw1qnGDgyITsYErfONKAHn05nv4= github.com/ClickHouse/ch-go v0.67.0 h1:18MQF6vZHj+4/hTRaK7JbS/TIzn4I55wC+QzO24uiqc= github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18= -github.com/ClickHouse/clickhouse-go/v2 v2.34.0 h1:Y4rqkdrRHgExvC4o/NTbLdY5LFQ3LHS77/RNFxFX3Co= -github.com/ClickHouse/clickhouse-go/v2 v2.34.0/go.mod h1:yioSINoRLVZkLyDzdMXPLRIqhDvel8iLBlwh6Iefso8= github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4= github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/sketches-go v1.4.6 h1:acd5fb+QdUzGrosfNLwrIhqyrbMORpvBy7mE+vHlT3I= github.com/DataDog/sketches-go v1.4.6/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg= @@ -557,17 +300,16 @@ github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g= github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0 h1:kAtNAWwvTt5+iew6baV0kbOrtjYTXPtWNSyOFlcxkBU= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRBTGfw0Ndv4YqNxlB8zAPJJDxbASE= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.6 h1:UucmvNRPE75F3KzT68GHhKzOPwttxiFkh1d5LTTywW8= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.6/go.mod h1:XGripOBEUAcge8IUWR/NMAB5qO9k82tkbpoewBpyjYQ= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.8 h1:9aTh5GPncdE8BjUn+xanuF/BT3m2BJiyvS50Mmws/fw= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.8/go.mod h1:exon/I6I+5u/ab7AHmGh0eCXGoYZO5cjqA3wHJlYFFQ= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0 h1:YVtMlmfRUTaWs3+1acwMBp7rBUo6zrxl6Kn13/R9YW4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0/go.mod h1:rKOFVIPbNs2wZeh7ZeQ0D9p/XLgbNiTr5m7x6KuAshk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator v0.53.0 h1:RAHqDHJmNMLe6JvDoRIlXmb72w+62Ue/k5p/qP9yfAg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator v0.53.0/go.mod h1:dtCRwgvytbGKWdlrjMOg9geBoRwRpCYWIOM/JhVsDIc= github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc= github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns= github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/1cE8= @@ -588,21 +330,22 @@ github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSr github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= -github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= -github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= +github.com/MissingRoberto/slog-gokit v0.0.0-20251105092822-783f72952ce4 h1:gTtFbl79tuZSeJuSO7kXSbmXSvKSa/PoUXda1tuz0O8= +github.com/MissingRoberto/slog-gokit v0.0.0-20251105092822-783f72952ce4/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 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/Sereal/Sereal/Go/sereal v0.0.0-20231009093132-b9187f1a92c6 h1:5kUcJJAKWWI82Xnp/CaU0eu5hLlHkmm9acjowSkwCd0= github.com/Sereal/Sereal/Go/sereal v0.0.0-20231009093132-b9187f1a92c6/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= @@ -610,87 +353,138 @@ github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjK github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/XSAM/otelsql v0.39.0 h1:4o374mEIMweaeevL7fd8Q3C710Xi2Jh/c8G4Qy9bvCY= +github.com/XSAM/otelsql v0.39.0/go.mod h1:uMOXLUX+wkuAuP0AR3B45NXX7E9lJS2mERa8gqdU8R0= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw= github.com/alecthomas/kong v1.10.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-filemutex v1.2.0/go.mod h1:mYyQSWvw9Tx2/H2n9qXPb52tTYfE0pZAWcBq5mK025c= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 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/aliyun/aliyun-oss-go-sdk v2.2.10+incompatible h1:ROMcuN61gI8SfQ+AEMh4d7GZ3gwTZLIhPjtd05TQCG4= github.com/aliyun/aliyun-oss-go-sdk v2.2.10+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/antchfx/xmlquery v1.4.3/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc= github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg= github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc= +github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4= github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/apache/arrow-go/v18 v18.3.0/go.mod h1:eEM1DnUTHhgGAjf/ChvOAQbUQ+EPohtDrArffvUjPg8= github.com/apache/arrow-go/v18 v18.4.0/go.mod h1:Aawvwhj8x2jURIzD9Moy72cF0FyJXOpkYpdmGRHcw14= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= -github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= -github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= -github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw= github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y= -github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1 h1:nMp7diZObd4XEVUR0pEvn7/E13JIgManMX79Q6quV6E= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI= +github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= +github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5 h1:oUEqVqonG3xuarrsze1KVJ30KagNYDemikTbdu8KlN8= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5/go.mod h1:VNM08cHlOsIbSHRqb6D/M2L4kKXfJv3A2/f0GNbOQSc= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.87 h1:oDPArGgCrG/4aTi86ij3S2PB59XXkTSKYVNQlmqRHXQ= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.87/go.mod h1:ZeQC4gVarhdcWeM1c90DyBLaBCNhEeAbKUXwVI/byvw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 h1:A99gjqZDbdhjtjJVZrmVzVKO2+p3MSg35bDWtbMQVxw= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 h1:0wOCTKrmwkyC8Bk76hYH/B4IJn5MGt6gMkSXc0A2uyc= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0 h1:JPXkrQk5OS/+Q81fKH97Ll/Vmmy0p9vwHhxw+V+tVjg= github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0/go.mod h1:dJngkoVMrq0K7QvRkdRZYM4NUp6cdWa2GBdpm8zoY8U= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4 h1:NgRFYyFpiMD62y4VPXh4DosPFbZd4vdMVBWKk0VmWXc= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4/go.mod h1:TKKN7IQoM7uTnyuFm9bm9cw5P//ZYTl4m3htBWQ1G/c= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2 h1:vlYXbindmagyVA3RS2SPd47eKZ00GZZQcr+etTviHtc= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2/go.mod h1:yGhDiLKguA3iFJYxbrQkQiNzuy+ddxesSZYWVeeEH5Q= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.7 h1:d+mnMa4JbJlooSbYQfrJpit/YINaB30JEVgrhtjZneA= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.7/go.mod h1:1X1NotbcGHH7PCQJ98PsExSxsJj/VWzz8MfFz43+02M= github.com/aws/aws-sdk-go-v2/service/sns v1.31.3 h1:eSTEdxkfle2G98FE+Xl3db/XAXXVTJPNQo9K/Ar8oAI= github.com/aws/aws-sdk-go-v2/service/sns v1.31.3/go.mod h1:1dn0delSO3J69THuty5iwP0US2Glt0mx2qBBlI13pvw= github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 h1:PajtbJ/5bEo6iUAIGMYnK8ljqg2F1h4mMCGh1acjN30= github.com/aws/aws-sdk-go-v2/service/sns v1.34.2/go.mod h1:PJtxxMdj747j8DeZENRTTYAz/lx/pADn/U0k7YNNiUY= +github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE= +github.com/aws/aws-sdk-go-v2/service/sns v1.34.7/go.mod h1:4WYoZAhHt+dWYpoOQUgkUKfuQbE6Gg/hW4oXE0pKS9U= github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3 h1:Vjqy5BZCOIsn4Pj8xzyqgGmsSqzz7y/WXbN3RgOoVrc= github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3/go.mod h1:L0enV3GCRd5iG9B64W35C4/hwsCB00Ib+DKVGTadKHI= github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 h1:j5BchjfDoS7K26vPdyJlyxBIIBGDflq3qjjJKBDlbcI= github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3/go.mod h1:Bar4MrRxeqdn6XIh8JGfiXuFRmyrrsZNTJotxEJmWW0= +github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ= +github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 h1:hgSBvRT7JEWx2+vEGI9/Ld5rZtl7M5lu8PqdvOmbRHw= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4/go.mod h1:v7NIzEFIHBiicOMaMTuEmbnzGnqW0d+6ulNALul6fYE= github.com/aws/aws-sdk-go-v2/service/ssm v1.58.0 h1:zQz6Q5uaC8s9734DV9UDAm2q1TEEfOvEejDBSulOapI= github.com/aws/aws-sdk-go-v2/service/ssm v1.58.0/go.mod h1:PUWUl5MDiYNQkUHN9Pyd9kgtA/YhbxnSnHP+yQqzrM8= +github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1 h1:OwMzNDe5VVTXD4kGmeK/FtqAITiV8Mw4TCa8IyNO0as= +github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1/go.mod h1:IyVabkWrs8SNdOEZLyFFcW9bUltV4G6OQS0s6H20PHg= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= +github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 h1:CJyGEyO1CIwOnXTU40urf0mchf6t3voxpvUDikOU9LY= github.com/awslabs/aws-lambda-go-api-proxy v0.16.2/go.mod h1:vxxjwBHe/KbgFeNlAP/Tvp4SsVRL3WQamcWRxqVh0z0= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/baidubce/bce-sdk-go v0.9.188 h1:8MA7ewe4VpX01uYl7Kic6ZvfIReUFdSKbY46ZqlQM7U= github.com/baidubce/bce-sdk-go v0.9.188/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= -github.com/bazelbuild/rules_go v0.49.0 h1:5vCbuvy8Q11g41lseGJDc5vxhDjJtfxr6nM/IC4VmqM= -github.com/bazelbuild/rules_go v0.49.0/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= github.com/benbjohnson/immutable v0.4.0 h1:CTqXbEerYso8YzVPxmWxh2gnoRQbbB9X1quUC8+vGZA= github.com/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= @@ -713,7 +507,6 @@ github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bsm/sarama-cluster v2.1.13+incompatible h1:bqU3gMJbWZVxLZ9PGWVKP05yOmFXUlfw61RBwuE3PYU= github.com/bsm/sarama-cluster v2.1.13+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM= -github.com/bufbuild/protovalidate-go v0.9.1 h1:cdrIA33994yCcJyEIZRL36ZGTe9UDM/WHs5MBHEimiE= github.com/bufbuild/protovalidate-go v0.9.1/go.mod h1:5jptBxfvlY51RhX32zR6875JfPBRXUsQjyZjm/NqkLQ= github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -721,31 +514,30 @@ github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9Mwe github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0= -github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= @@ -753,25 +545,34 @@ github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ= github.com/coder/quartz v0.1.0/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= -github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= -github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= -github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/btrfs/v2 v2.0.0/go.mod h1:swkD/7j9HApWpzl8OHfrHNxppPd9l44DFZdF94BUj9k= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd v1.6.18 h1:qZbsLvmyu+Vlty0/Ex5xc0z2YtKpIsb5n45mAMI+2Ns= -github.com/containerd/containerd v1.6.18/go.mod h1:1RdCUu95+gc2v9t3IL+zIlpClSmew7/0YS8O5eQZrOw= github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/containerd/go-cni v1.1.9/go.mod h1:XYrZJ1d5W6E2VOvjffL3IZq0Dz6bsVlERHbekNK90PM= +github.com/containerd/imgcrypt v1.1.8/go.mod h1:x6QvFIkMyO2qGIY2zXc88ivEzcbgvLdWjoZyGqDap5U= +github.com/containerd/nri v0.6.1/go.mod h1:7+sX3wNx+LR7RzhjnJiUkFDhn18P5Bg/0VnJ/uXpRJM= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/containerd/ttrpc v1.2.4/go.mod h1:ojvb8SJBSch0XkqNO0L0YX/5NxR3UnVk2LzFKBK0upc= +github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= +github.com/containerd/zfs v1.1.0/go.mod h1:oZF9wBnrnQjpWLaPKEinrx3TQ9a+W/RJO7Zb41d8YLE= +github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw= +github.com/containernetworking/plugins v1.2.0/go.mod h1:/VjX4uHecW5vVimFa1wkG4s+r/s9qIfPdqlLF4TW8c4= +github.com/containers/ocicrypt v1.1.10/go.mod h1:YfzSSr06PTHQwSTUKqDSjish9BeW1E4HUmreluQcMd8= github.com/coreos/etcd v3.3.27+incompatible h1:QIudLb9KeBsE5zyYxd1mjzRSkzLg9Wf9QlRwFgd6oTA= github.com/coreos/etcd v3.3.27+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= +github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0= github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= @@ -782,15 +583,11 @@ github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bOD 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/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= @@ -801,14 +598,12 @@ github.com/crossdock/crossdock-go v0.0.0-20160816171116-049aabb0122b h1:WR1qVJzb github.com/crossdock/crossdock-go v0.0.0-20160816171116-049aabb0122b/go.mod h1:v9FBN7gdVTpiD/+LZ7Po0UKvROyT87uLVxTHVky/dlQ= github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= -github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo= -github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI= github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= -github.com/cucumber/messages/go/v22 v22.0.0 h1:hk3ITpEWQ+KWDe619zYcqtaLOfcu9jgClSeps3DlNWI= github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f h1:7uSNgsgcarNk4oiN/nNkO0J7KAjlsF5Yv5Gf/tFdHas= github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4 h1:CVAqftqbj+exlab+8KJQrE+kNIVlQfJt58j4GxCMF1s= @@ -823,12 +618,13 @@ github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892 h1:qg9VbHo1TlL0KDM0vYvBG9EY0X0Yku5WYIPoFWt8f6o= github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= -github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= -github.com/dgryski/go-ddmin v0.0.0-20210904190556-96a6d69f1034 h1:BuCyszxPxUjBrYW2HNVrimC0rBUs2U27jCJGVh0IKTM= +github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgryski/go-ddmin v0.0.0-20210904190556-96a6d69f1034/go.mod h1:zz4KxBkcXUWKjIcrc+uphJ1gPh/t18ymGm3PmQ+VGTk= github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b h1:Yqiad0+sloMPdd/0Fg22actpFx0dekpzt1xJmVNVkU0= github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= @@ -837,28 +633,30 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/ github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dmarkham/enumer v1.5.11 h1:quorLCaEfzjJ23Pf7PB9lyyaHseh91YfTM/sAD/4Mbo= -github.com/dmarkham/enumer v1.5.11/go.mod h1:yixql+kDDQRYqcuBM2n9Vlt7NoT9ixgXhaXry8vmRg8= github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/docker/cli v23.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.7.0+incompatible h1:neUDAlf3wX6Ml4HdqTrbcOHXtfRN0TFIwt6YFL7N9RU= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 h1:IMfrF5LCzP2Vhw7j4IIH3HxPsCLuZYjDqFAM/C88ulg= github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8/go.mod h1:LFyLie6XcDbyKGeVK6bHe+9aJTYCxWLBg5IrJZOaXKA= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790 h1:zxMsH7RLiG+dlZ/y0LgJHTV26XoiSJcuWq+em6t6VVc= +github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= +github.com/dolthub/go-mysql-server v0.20.1-0.20251009205227-b4366f30538c h1:vElww7wlYrlu1dldciCcYOvVuh73gw8i6mkcTUvH6nQ= +github.com/dolthub/go-mysql-server v0.20.1-0.20251009205227-b4366f30538c/go.mod h1:EeYR0apo+8j2Dyxmn2ghkPlirO2S5mT1xHBrA+Efys8= github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 h1:7/v8q9XGFa6q5Ap4Z/OhNkAMBaK5YeuEzwJt+NZdhiE= github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY= github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= +github.com/dolthub/vitess v0.0.0-20250410090211-143e6b272ad4/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= -github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f h1:/jEs7lulqVO2u1+XI5rW4oFwIIusxuDOVKD9PAzlW2E= -github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f/go.mod h1:nDRkX7PHq+p39AD5/usv3KZMerxZTYU/9rfLS5IDspU= -github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= @@ -868,8 +666,6 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1 github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b h1:ZHiD4/yE4idlbqvAO6iYCOYRzOMRpxkW+FKasRA3tsQ= github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b/go.mod h1:OmVcnJopJL8d3X3sSXTiypGoUSgFq1aDGmlrdi9dn/M= github.com/elastic/elastic-transport-go/v8 v8.6.1 h1:h2jQRqH6eLGiBSN4eZbQnJLtL4bC5b4lfVFRjw2R4e4= @@ -878,31 +674,24 @@ github.com/elastic/go-elasticsearch/v8 v8.17.1 h1:bOXChDoCMB4TIwwGqKd031U8OXssmW github.com/elastic/go-elasticsearch/v8 v8.17.1/go.mod h1:MVJCtL+gJJ7x5jFeUmA20O7rvipX8GcQmo5iBcmaJn4= github.com/elastic/go-grok v0.3.1 h1:WEhUxe2KrwycMnlvMimJXvzRa7DoByJB4PVUIE1ZD/U= github.com/elastic/go-grok v0.3.1/go.mod h1:n38ls8ZgOboZRgKcjMY8eFeZFMmcL9n2lP0iHhIDk64= -github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM= -github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn1XU0= -github.com/elastic/go-sysinfo v1.15.3/go.mod h1:K/cNrqYTDrSoMh2oDkYEMS2+a72GRxMvNP+GC+vRIlo= github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q= github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= -github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= github.com/elastic/lunes v0.1.0 h1:amRtLPjwkWtzDF/RKzcEPMvSsSseLDLW+bnhfNSLRe4= github.com/elastic/lunes v0.1.0/go.mod h1:xGphYIt3XdZRtyWosHQTErsQTd4OP1p9wsbVoHelrd4= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= -github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= -github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= -github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= -github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= -github.com/expr-lang/expr v1.17.0/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= -github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= @@ -910,20 +699,21 @@ 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.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c h1:yKN46XJHYC/gvgH2UsisJ31+n4K3S7QYZSfU2uAWjuI= github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c/go.mod h1:L92h+dgwElEyUuShEwjbiHjseW410WIcNz+Bjutc8YQ= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= github.com/fsouza/fake-gcs-server v1.52.2 h1:j6ne83nqHrlX5EEor7WWVIKdBsztGtwJ1J2mL+k+iio= github.com/fsouza/fake-gcs-server v1.52.2/go.mod h1:47HKyIkz6oLTes1R8vEaHLwXfzYsGfmDUk1ViHHAUsA= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -940,7 +730,7 @@ github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXW github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= -github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= @@ -948,15 +738,20 @@ github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1: github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -965,24 +760,20 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= -github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= 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-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= -github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7 h1:TvUE5vjfoa7fFHMlmGOk0CsauNj1w4yJjR9+/GnWVCw= -github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= github.com/gocraft/dbr/v2 v2.7.2 h1:ccUxMuz6RdZvD7VPhMRRMSS/ECF3gytPhPtcavjktHk= github.com/gocraft/dbr/v2 v2.7.2/go.mod h1:5bCqyIXO5fYn3jEp/L06QF4K1siFdhxChMjdNu6YJrg= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -991,19 +782,25 @@ github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= -github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= -github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk= github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= @@ -1011,17 +808,12 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/googleapis/cloud-bigtable-clients-test v0.0.3 h1:afMKTvA/jc6jSTMkeHBZGFDTt8Cc+kb1ATFzqMK85hw= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= github.com/googleapis/cloud-bigtable-clients-test v0.0.3/go.mod h1:TWtDzrrAI70C3dNLDY+nZN3gxHtFdZIbpL9rCTFyxE0= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= -github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0= github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= @@ -1035,70 +827,55 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e/go.mod h1:HfvjmU3UqCIpoy9Z2wgKGrZ4A5vz+yQlP9ZXvCfEkiA= -github.com/grafana/alerting v0.0.0-20250403153742-418bc7118d05 h1:hMzOzI/S0nkZt0nUqpfAa4Rdb+YL8z8oG3pl4Jb31h8= -github.com/grafana/alerting v0.0.0-20250403153742-418bc7118d05/go.mod h1:K3YAJumchx5EEZItGv4D3pCv/Ux796hmoOibP/p/eYk= -github.com/grafana/alerting v0.0.0-20250429131604-de176b4a0309 h1:H2p3XKDHnTBGkMXLCgXiqb2dFnHbQ4zPDXOwKK4Ne3Y= -github.com/grafana/alerting v0.0.0-20250429131604-de176b4a0309/go.mod h1:pMfhRxL2LZ3Pm8iy7VcVsb9CLYuBtjFYbf1oxgx7yFA= -github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945 h1:3imTbxFpZSVI6IBIB9mn+Xc40lUweWjfMaBSgXR7rLs= -github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945/go.mod h1:gtR7agmxVfJOmNKV/n2ZULgOYTYNL+PDKYB5N48tQ7Q= -github.com/grafana/alerting v0.0.0-20250709204613-c5c6f9c1653d/go.mod h1:gtR7agmxVfJOmNKV/n2ZULgOYTYNL+PDKYB5N48tQ7Q= -github.com/grafana/alerting v0.0.0-20250911172908-2b26ef8f17eb/go.mod h1:XWqj/rlsy4OV/E9XNNyFn+a7U4GNsSugPb2rDBj9+58= -github.com/grafana/alerting v0.0.0-20250923203439-adb598e7d509 h1:8JMtYCClxrxRXsF5jc64GTURZFHJHFK/kzC7joRNTtI= -github.com/grafana/alerting v0.0.0-20250923203439-adb598e7d509/go.mod h1:T5sitas9VhVj8/S9LeRLy6H75kTBdh/sCCqHo7gaQI8= -github.com/grafana/alerting v0.0.0-20250925193206-bd061d3d9185 h1:R494uXJOz7glN76hJXKjbwu+VBYFsT0CFprsXmdHla0= -github.com/grafana/alerting v0.0.0-20250925193206-bd061d3d9185/go.mod h1:T5sitas9VhVj8/S9LeRLy6H75kTBdh/sCCqHo7gaQI8= -github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= -github.com/grafana/authlib v0.0.0-20250909101823-1b466dbd19a1/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A= -github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c/go.mod h1:C6CmTG6vfiqebjJswKsc6zes+1F/OtTCi6aAtL5Um6A= -github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= -github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= -github.com/grafana/authlib/types v0.0.0-20250314102521-a77865c746c0/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= -github.com/grafana/authlib/types v0.0.0-20250721184729-1593a38e4933/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= -github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls= +github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= +github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw= -github.com/grafana/cog v0.0.37/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= -github.com/grafana/cog v0.0.38 h1:V7gRRn/mh7Bg1ptrCxo0bv6K0SnG9TiDZk+3Ppftn6s= -github.com/grafana/cog v0.0.38/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= -github.com/grafana/cog v0.0.40/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= -github.com/grafana/cog v0.0.41/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= github.com/grafana/dskit v0.0.0-20250818234656-8ff9c6532e85/go.mod h1:kImsvJ1xnmeT9Z6StK+RdEKLzlpzBsKwJbEQfmBJdFs= github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= +github.com/grafana/go-mysql-server v0.20.1-0.20251027172658-317a8d46ffa4/go.mod h1:EeYR0apo+8j2Dyxmn2ghkPlirO2S5mT1xHBrA+Efys8= github.com/grafana/gomemcache v0.0.0-20250228145437-da7b95fd2ac1/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= +github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0= github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0UrQVTqtYBU= github.com/grafana/grafana-app-sdk v0.41.0/go.mod h1:Wg/3vEZfok1hhIWiHaaJm+FwkosfO98o8KbeLFEnZpY= github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= +github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= github.com/grafana/grafana-app-sdk/logging v0.38.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+V54zWQk/Q2/JsJQB4= github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= github.com/grafana/grafana-app-sdk/logging v0.43.0/go.mod h1:0xrjKSGY5z+NLGuGsXQpxiCHR4Smu79i/CbAfdkaB1M= github.com/grafana/grafana-app-sdk/logging v0.43.1/go.mod h1:0xrjKSGY5z+NLGuGsXQpxiCHR4Smu79i/CbAfdkaB1M= github.com/grafana/grafana-app-sdk/logging v0.43.2/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.48.0 h1:xolkQxBlA2LQF4hprKIAeu+zUem1DigYZ6XC1TOhFJE= +github.com/grafana/grafana-app-sdk/logging v0.48.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/plugin v0.41.0 h1:ShUvGpAVzM3UxcsfwS6l/lwW4ytDeTbCQXf8w2P8Yp8= github.com/grafana/grafana-app-sdk/plugin v0.41.0/go.mod h1:YIhimVfAqtOp3kdhxOanaSZjypVKh/bYxf9wfFfhDm0= github.com/grafana/grafana-aws-sdk v0.38.2 h1:TzQD0OpWsNjtldi5G5TLDlBRk8OyDf+B5ujcoAu4Dp0= github.com/grafana/grafana-aws-sdk v0.38.2/go.mod h1:j3vi+cXYHEFqjhBGrI6/lw1TNM+dl0Y3f0cSnDOPy+s= github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9yhDIpkss= github.com/grafana/grafana-aws-sdk v1.0.2/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= +github.com/grafana/grafana-aws-sdk v1.1.0/go.mod h1:7e+47EdHynteYWGoT5Ere9KeOXQObsk8F0vkOLQ1tz8= +github.com/grafana/grafana-aws-sdk v1.2.0/go.mod h1:bBo7qOmM3f61vO+2JxTolNUph1l2TmtzmWcU9/Im+8A= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= +github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8= github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana-plugin-sdk-go v0.269.1/go.mod h1:yv2KbO4mlr9WuDK2f+2gHAMTwwLmLuqaEnrPXTRU+OI= github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagIr8sWr1RT9h7ufJl5NMcWJAU= +github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= github.com/grafana/grafana-plugin-sdk-go v0.279.0/go.mod h1:/7oGN6Z7DGTGaLHhgIYrRr6Wvmdsb3BLw5hL4Kbjy88= +github.com/grafana/grafana-plugin-sdk-go v0.280.0/go.mod h1:Z15Wiq3c4I0tzHYrLYpOqrO8u3+2RJ+HN2Q9uiZTILA= github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A= @@ -1124,12 +901,15 @@ github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed h1:59JF1WhHLT+lNX8 github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed/go.mod h1:OIAAKNgG5fpuJQRNO1lUSj9nc18Xl3O7M8fjIlBO1cI= github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5 h1:MAQ2B0cu0V1S91ZjVa7NomNZFjaR2SmdtvdwhqBtyhU= github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco= +github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= 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/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 h1:4/BZkGObFWZf4cLbE2Vqg/1VTz67Q0AJ7LHspWLKJoQ= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= @@ -1139,14 +919,12 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= -github.com/hamba/avro/v2 v2.28.0 h1:E8J5D27biyAulWKNiEBhV85QPc9xRMCUCGJewS0KYCE= -github.com/hamba/avro/v2 v2.28.0/go.mod h1:9TVrlt1cG1kkTUtm9u2eO5Qb7rZXlYzoKqPt8TSH+TA= github.com/hamba/avro/v2 v2.29.0 h1:fkqoWEPxfygZxrkktgSHEpd0j/P7RKTBTDbcEeMdVEY= github.com/hamba/avro/v2 v2.29.0/go.mod h1:Pk3T+x74uJoJOFmHrdJ8PRdgSEL/kEKteJ31NytCKxI= github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY= @@ -1159,13 +937,16 @@ github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYi github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= github.com/hashicorp/go-msgpack/v2 v2.1.1/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE= github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= @@ -1175,6 +956,8 @@ github.com/hashicorp/raft v1.7.0/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK github.com/hashicorp/raft-wal v0.4.1 h1:aU8XZ6x8R9BAIB/83Z1dTDtXvDVmv9YVYeXxd/1QBSA= github.com/hashicorp/raft-wal v0.4.1/go.mod h1:A6vP5o8hGOs1LHfC1Okh9xPwWDcmb6Vvuz/QyqUXlOE= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/heroku/x v0.0.61 h1:yfoAAtnFWSFZj+UlS+RZL/h8QYEp1R4wHVEg0G+Hwh4= github.com/heroku/x v0.0.61/go.mod h1:C7xYbpMdond+s6L5VpniDUSVPRwm3kZum1o7XiD5ZHk= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -1189,31 +972,28 @@ github.com/influxdata/tdigest v0.0.2-0.20210216194612-fc98d27c9e8b h1:i44CesU68Z github.com/influxdata/tdigest v0.0.2-0.20210216194612-fc98d27c9e8b/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y= github.com/influxdata/telegraf v1.16.3 h1:x0qeuSGGMg5y+YqP/5ZHwXZu3bcBrO8AAQOTNlYEb1c= github.com/influxdata/telegraf v1.16.3/go.mod h1:fX/6k7qpIqzVPWyeIamb0wN5hbwc0ANUaTS80lPYFB8= +github.com/intel/goresctrl v0.3.0/go.mod h1:fdz3mD85cmP9sHD8JUlrNWAxvwM86CrbmVXltEKd7zk= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= -github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= -github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= -github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= -github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= -github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jon-whit/go-grpc-prometheus v1.4.0 h1:/wmpGDJcLXuEjXryWhVYEGt9YBRhtLwFEN7T+Flr8sw= github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY= +github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= -github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= -github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d h1:c93kUJDtVAXFEhsCh5jSxyOJmFHuzcihnslQiX8Urwo= @@ -1233,23 +1013,25 @@ github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIR github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kevinmbeaulieu/eq-go v1.0.0 h1:AQgYHURDOmnVJ62jnEk0W/7yFKEn+Lv8RHN6t7mB0Zo= github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= -github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a h1:K0EAzgzEQHW4Y5lxrmvPMltmlRDzlhLfGmots9EHUTI= -github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knadh/koanf v1.4.0/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs= 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/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= @@ -1277,12 +1059,8 @@ github.com/kr/pty v1.1.5 h1:hyz3dwM5QLc1Rfoz4FuWJQG5BN7tc6K1MndAUnGpQr4= github.com/kshvakov/clickhouse v1.3.5 h1:PDTYk9VYgbjPAWry3AoDREeMgOVUFij6bh6IjlloHL0= github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w= github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E= -github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= -github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-syslog/v4 v4.1.0 h1:Wsl194qyWXr7V6DrGWC3xmxA9Ra6XgWO+toNt2fmCaI= @@ -1291,23 +1069,27 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b h1:11UHH39z1RhZ5dc4y4r/4koJo6IYFgTRMe/LlwRTEw0= github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/go-expohisto v1.0.0 h1:UPtTS1rGdtehbbAF7o/dhkWLTDI73UifG8LbfQI7cA4= github.com/lightstep/go-expohisto v1.0.0/go.mod h1:xDXD0++Mu2FOaItXtdDfksfgxfV0z1TMPa+e/EUd0cs= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA= github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f h1:WVPqVsbUsrzAebTEgWRAZMdDOfkFx06iyhbIoyMgtkE= github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f/go.mod h1:aS446i8akEg0DAtNKTVYpNpLPMc0SzsZ0RtGhjl0uFM= -github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= -github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= -github.com/madflojo/testcerts v1.1.1 h1:YsSHWV79nMNZK0mJtwXjKoYHjJEbLPFefR8TxmmWupY= -github.com/madflojo/testcerts v1.1.1/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLDUsQhpmkuo0= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/matryer/moq v0.5.2 h1:b2bsanSaO6IdraaIvPBzHnqcrkkQmk1/310HdT2nNQs= @@ -1315,21 +1097,26 @@ github.com/matryer/moq v0.5.2/go.mod h1:W/k5PLfou4f+bzke9VPXTbfJljxoeR1tLHigsmbs github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU= github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ= github.com/mfridman/xflag v0.1.0 h1:TWZrZwG1QklFX5S4j1vxfF1sZbZeZSGofMwPMLAF29M= github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/minio/minio-go/v7 v7.0.75/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= @@ -1338,22 +1125,23 @@ github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWe github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mithrandie/readline-csvq v1.3.0 h1:VTJEOGouJ8j27jJCD4kBBbNTxM0OdBvE1aY1tMhlqE8= github.com/mithrandie/readline-csvq v1.3.0/go.mod h1:FKyYqDgf/G4SNov7SMFXRWO6LQLXIOeTog/NB97FZl0= -github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615 h1:/mD+ABZyXD39BzJI2XyRJlqdZG11gXFo0SSynL+OFeU= -github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615/go.mod h1:Ad7oeElCZqA1Ufj0U9/liOF4BtVepxRcTvr2ey7zTvM= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs= github.com/moby/moby v27.5.1+incompatible h1:/pN59F/t3U7Q4FPzV88nzqf7Fp0qqCSL2KzhZaiKcKw= github.com/moby/moby v27.5.1+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/moby v28.0.1+incompatible h1:10ejBTwFhM3/9p6pSaKrLyXnx7QzzCmCYHAedOp67cQ= +github.com/moby/moby v28.0.1+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= +github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok= +github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= @@ -1362,7 +1150,6 @@ github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1 github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= github.com/ncw/swift v1.0.53 h1:luHjjTNtekIEvHg5KdAFIBaH7bWfNkefwFnpDffSIks= github.com/ncw/swift v1.0.53/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/ncw/swift/v2 v2.0.2 h1:jx282pcAKFhmoZBSdMcCRFn9VWkoBIRsCpe+yZq7vEk= @@ -1371,23 +1158,33 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wu github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c h1:bY6ktFuJkt+ZXkX0RChQch2FtHpWQLVS8Qo1YasiIVk= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/networkplumbing/go-nft v0.2.0/go.mod h1:HnnM+tYvlGAsMU7yoYwXEVLLiDW9gdMmb5HoGcwpuQs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/olivere/elastic v6.2.37+incompatible h1:UfSGJem5czY+x/LqxgeCBgjDn6St+z8OnsCuxwD3L0U= github.com/olivere/elastic v6.2.37+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= github.com/open-feature/go-sdk v1.15.1/go.mod h1:2WAFYzt8rLYavcubpCoiym3iSCXiHdPB6DxtMkv2wyo= +github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3/go.mod h1:dPUHjAIFzg+ci/wt6XxlNiiMkOh5Yw4SGyeRY0AFT0g= +github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE= +github.com/open-policy-agent/opa v0.42.2/go.mod h1:MrmoTi/BsKWT58kXlVayBb+rYVeaMwuBm3nYAN3923s= github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.121.0 h1:gX7HGoRE0OTMS8ZVh/zPeQe+ZEASKxAo6Wn9dIcsgRE= github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.121.0/go.mod h1:y6UqtUREKcyDzLPQC5wSPbOlZR2HLUrNp/2ITZalVKA= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.46.0/go.mod h1:V6s9e+E0TnLUGq+rq2JdAEzdZHIlV1WYKo8z13hjZ1o= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.121.0 h1:1J3XbT944dDqif4TINht6SBz1l+HXpOeyZglaUfdY+0= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.121.0/go.mod h1:60Jb5QNQwPeb2OKfVi0M0u0bSqmQiQU/suZQD8lOMZo= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.121.0 h1:t4auPAdvyl437eQQ6P0SzZRn55/d439WK9268p3bJdc= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.121.0/go.mod h1:fFtAEjQygwWnabKV++P/OqokQ8YlotrTWQFdT5hcc4M= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.46.0/go.mod h1:otkXOOliU7MTxmoKNd6CQ6Jz1x+cYunc4EGJTbtLF2g= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.124.1 h1:+aiMrDR6xiaDM7xN4ByrBYI0Craqt68nZicmpYpt0co= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.124.1/go.mod h1:H/TEWN4jgExt0McrtrBK2VFK6r9LRsWtqhEZrH690rs= github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckv2extension v0.121.0 h1:zI2FKKFUnzzfzgKODlMdKyeOZx+kWQROE2OP1pttZPQ= @@ -1398,36 +1195,46 @@ github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.121 github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.121.0/go.mod h1:f9MPSdMvooe2s9Zg0WVBJOvg72d9fFikxFt7yniFros= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.121.0 h1:I+F6xdXQsiXXdce7yjHN+y4LX5MrZI1kNmhBunJffdA= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.121.0/go.mod h1:cRh3l2emFBwW96dHnlPLr1psbEYjYJmn5qFujOkbfRo= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.121.0/go.mod h1:0pSZ+iLy8jYSKCmjlqT/5sBTXkq5xoxuRfamTardurg= github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.124.1 h1:2uQmRiQ7EV7s1slz7fEvAVhJIFTyExnj/4unfw0Era8= github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter v0.124.1/go.mod h1:VqJ9CWEwk6N7YzumaV1gbxY0UeLlnzAzYmTw8ieaIYs= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka v0.121.0/go.mod h1:GusNPtNoz5/2eC9G8qq/yqG8AK/s1hWVta5qomuC1f0= github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka v0.124.1 h1:hbJs+dkXx3bPshr614bYk6bwvtM9YruaGqhwXWP2UuU= github.com/open-telemetry/opentelemetry-collector-contrib/internal/kafka v0.124.1/go.mod h1:oDdNpMFQ6hT0IsGDRHkG5mQ3DLxF6kYvDuoEIcoQbCo= github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdatautil v0.121.0 h1:AY0metK/HpBqnqv6H8jgY7JTEXnc7cNdKWYZsDArXJE= github.com/open-telemetry/opentelemetry-collector-contrib/internal/pdatautil v0.121.0/go.mod h1:xVMTbBp/I+Y0j1hE05Vm4mk32t80Z4Ee8/rybGjYDM0= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.46.0/go.mod h1:GTT5wN6i6J0veMFn7WEAZn4lqSjyYELMuvWP9pkhsXs= github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.124.1 h1:QUXMxZTjTER4vDa4ldkSSAd89blgS5974j5AkRLp2M0= github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.124.1/go.mod h1:jRQpw8VJDmUmyH8ONQ5FmhYEXkaxzMFX6Tei8N4GDZs= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.121.0 h1:dnRf9xyjTBGHui949SS0A87fNa49Al32dog9XmeGY7I= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0.121.0/go.mod h1:ZAFQ1Te56fodNun+lmC/jFjVxhecumV882ct8sbdeAM= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.123.0/go.mod h1:EAzpBsYIXoXQbyVTkvfOksaiq+jctnoxnPevB+/xsqw= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.124.1 h1:QX88VjAiUaG7SQe4A14b3JPgvzZK6OXpWoBd32Zlp1o= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.124.1/go.mod h1:jRsmTRbn1HkrOpoeXS0KGfXobrh7E3W/p9fLwLdpcB8= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.121.0 h1:7xzxbwIBpb8LlMNY+RIdEnWTJH+pDu0LiL+tcIziEkA= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.121.0/go.mod h1:gePUJjHVTT0N6ruTBSA3vDlDlh+TJ4tGt/mm7o1MIuQ= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.121.0/go.mod h1:oPNQ3V2N/I9k1ThtYjw3nTDF+BHZRw0uAj395CBwuWo= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.124.1 h1:oiLa2lg+Ix+wHi4bPsnQ5+DJc6u+OHZt6YhP/rjgbNc= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl v0.124.1/go.mod h1:v8WbCvCAzp8OgxdyXSZlLUkEKJJOLhlOF57kRCVUoAQ= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.123.0/go.mod h1:yv3tSTQdVXHvNT+OZ95Drnwb5ntWdlhTthKAi/sB5QY= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.124.1 h1:G2daAIXiQhAwQSz9RK71QsBH9rmH/m/vdkFuGIEPfS4= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.124.1/go.mod h1:/WAA1PKvHNz7E5SrtGg2KfAWl/PrmS0FVYOanoGxk0I= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.123.0/go.mod h1:hXC2pbybf1KcGnXB5RvHhR++/QMgS1HP0iFf/DSXBFg= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.124.1 h1:mMVzpkpy6rKL1Q/xXNogZVtWebIlxTRzhsgp3b9ioCM= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.124.1/go.mod h1:jM8Gsd0fIiwRzWrzd7Gm6PZYi5AgHPRkz0625Rtqyxo= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.121.0 h1:9cNRnGUjm9jh8zPyMRxTSLIqP+mpY2KVJSjwq4sGJIA= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.121.0/go.mod h1:JjmyxvVh1wwMNnN+KXYUZGNkU/L779q8Yb7lNsB4KSk= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.121.0 h1:Gv2xDvT8r/zH7MzGRcMO4ckmP1UpNNbEfN7fKx3vqUQ= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status v0.121.0/go.mod h1:PQjfR4WB9Q1MR9X/6IqxWGpKfavp9OF1fUcAqnPGGpQ= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.121.0/go.mod h1:yG4ykQmlkpLKSJW3/a+P/DfDKcKvxLWvNfNJDHZvayg= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.124.1 h1:kPPjaOlncaJjoaJRUzW3TbqHM4v/+TtxC2Bzpa7z9X4= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure v0.124.1/go.mod h1:+6zqsZ1YGNsl7RUxfjzg7MKEWd5J6QAlq7zM3Nqy8UI= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.46.0/go.mod h1:5j+TUBWjHWhRB4hmSrCQbD4q4lg/FF2+L8wYkaiRDhI= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.124.1 h1:IYxc8uPVCtKlgxjbYFGZ5/wXLdwAH1WQPKRlzaXtr8E= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.124.1/go.mod h1:jFRiSEn2ss3e4CWG6GTDOJMYRI0C/N2XbMMIBzZuqZY= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.121.0 h1:QzV771eoAx2W9HCFq/YJVMNgEuG4lvalPTQIutheFhc= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.121.0/go.mod h1:+HFLuxyyP0bSsnx/L7ATAukEPQ8+05kcOps8mJeYPb8= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.121.0/go.mod h1:377OdmAoSb+F2uYC1h1ZFSGzZ3qxIroAfp+PK0Kp4J8= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.124.1 h1:cyvsrj/D+/XwoXSw/4FD/S1M50jco7k963f/+si907w= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.124.1/go.mod h1:xAU/ievEszZYP1DGWy+yYjAjfjRCKyBLBAnXYvKWNWc= github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor v0.121.0 h1:/d5mamybZt/HWFUhn9khuHf9aDYO6UFmy56Kz65FD4c= @@ -1438,14 +1245,24 @@ github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterproces github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.124.1/go.mod h1:B/GP3l4Y1qNsNtWVIzpwS8jWB1Nn/vx0sFBlVDkWt9E= github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.121.0 h1:CDMMmOdUF3EPvhNFBMfPrSMvqBp97FeATs/kdVnqHGc= github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.121.0/go.mod h1:qvnIa2vzFlLKsILRWHKubjHIWfWWRAiNZtXdaa/oSEc= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.121.0/go.mod h1:zwefGutoIIGNAIK/MfqcSFazFyGQcvMNxRH8gEN/0c4= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.124.1 h1:r20zOMMBcxzI2Sni2HyTR7TVxuNXLn1Ov6qtnlVWoJc= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.124.1/go.mod h1:1UsKa4xSNodA2i6ic5mmyd31khXyTGxsVKJoKmq/jtU= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.121.0/go.mod h1:G9Yj59SZ6Gh/WlBzy20RvFZWKEd0KCiqgH4OLd9zI7A= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.124.1 h1:Pdlf5D5gB/5Khi3t5PiojQrCulcEejecsxoSpOd0ekA= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.124.1/go.mod h1:pKNXK34B2dV0nEDAFgHtYQHBlZpQlLWcKFZpblY3xQs= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.46.0/go.mod h1:txkHYpFohW+zOtO3VipamUOPEJ2eyzpqQKw/eJ2Ajj8= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.124.1 h1:npAUCN3GM93FCmSZsODhMuNn+v2xDYQTQlgRg/LqBfU= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.124.1/go.mod h1:4+9pSfniXXdRpkKf0QNdElOd7yIWD4ux8D260tSPV54= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.121.0/go.mod h1:3axnebi8xUm9ifbs1myzehw2nODtIMrQlL566sJ4bYw= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.124.1 h1:XkxqUEoukMWXF+EpEWeM9itXKt62yKi13Lzd8ZEASP4= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.124.1/go.mod h1:CuCZVPz+yn88b5vhZPAlxaMrVuhAVexUV6f8b07lpUc= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= +github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w= github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= @@ -1455,12 +1272,8 @@ github.com/pablor21/echo-etag/v4 v4.0.3 h1:o49j5NmxbqWIMfKHtzJan33PW12LQnORDFlM6 github.com/pablor21/echo-etag/v4 v4.0.3/go.mod h1:cKXqBSw57xk+jT68+CR9HZTD4yWgbmGjvDdQcxRWdY0= github.com/parquet-go/parquet-go v0.25.1-0.20250428214007-401fed3de956 h1:EqOiLPZlZ3UfC9d51fuYWP13FxwSzUCTArR0wg1LlxM= github.com/parquet-go/parquet-go v0.25.1-0.20250428214007-401fed3de956/go.mod h1:OqBBRGBl7+llplCvDMql8dEKaDqjaFA/VAPw+OJiNiw= -github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0= -github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= -github.com/paulmach/protoscan v0.2.1 h1:rM0FpcTjUMvPUNk2BhPJrreDKetq43ChnL+x1sRg8O8= -github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw= @@ -1470,31 +1283,29 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= -github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= -github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20= github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8= -github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ= github.com/prometheus/statsd_exporter v0.26.1 h1:ucbIAdPmwAUcA+dU+Opok8Qt81Aw8HanlO+2N/Wjv7w= github.com/prometheus/statsd_exporter v0.26.1/go.mod h1:XlDdjAmRmx3JVvPPYuFNUg+Ynyb5kR69iPPkQjxXFMk= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= @@ -1504,8 +1315,6 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= -github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA= github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= @@ -1514,9 +1323,6 @@ github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= -github.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM= -github.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c= github.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QHTU= github.com/relvacode/iso8601 v1.6.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/richardartoul/molecule v1.0.0 h1:+LFA9cT7fn8KF39zy4dhOnwcOwRoqKiBkPqKqya+8+U= @@ -1525,13 +1331,13 @@ github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8= -github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 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= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= +github.com/safchain/ethtool v0.2.0/go.mod h1:WkKB1DnNtvsMlDmQ50sgwowDJV/hGbJSOvJoEXs1AJQ= github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= @@ -1549,43 +1355,42 @@ github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXI github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= +github.com/segmentio/parquet-go v0.0.0-20220811205829-7efc157d28af/go.mod h1:PxYdAI6cGd+s1j4hZDQbz3VFgobF5fDA0weLeNWKTE4= +github.com/sercand/kuberesolver v2.4.0+incompatible h1:WE2OlRf6wjLxHwNkkFLQGaZcVLEXjMjBPjjEU5vksH8= +github.com/sercand/kuberesolver v2.4.0+incompatible/go.mod h1:lWF3GL0xptCB/vCiJPl/ZshwPsX/n4Y7u0CW9E7aQIQ= github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= -github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= -github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/substrait-io/substrait v0.66.1-0.20250205013839-a30b3e2d7ec6 h1:XqtxwYFCjS4L0o1QD4ipGHCuFG94U0f6BeldbilGQjU= -github.com/substrait-io/substrait v0.66.1-0.20250205013839-a30b3e2d7ec6/go.mod h1:MPFNw6sToJgpD5Z2rj0rQrdP/Oq8HG7Z2t3CAEHtkHw= github.com/substrait-io/substrait v0.69.0 h1:qfwUe1qKa3PsCclMpubQOF6nqIqS14geUuvzJ1P7gsM= github.com/substrait-io/substrait v0.69.0/go.mod h1:MPFNw6sToJgpD5Z2rj0rQrdP/Oq8HG7Z2t3CAEHtkHw= -github.com/substrait-io/substrait-go/v3 v3.9.1 h1:2yfHDHpK6KMcvLd0bJVzUJoeXO+K98yS+ciBruxD9po= -github.com/substrait-io/substrait-go/v3 v3.9.1/go.mod h1:VG7jCqtUm28bSngHwq86FywtU74knJ25LNX63SZ53+E= -github.com/substrait-io/substrait-go/v4 v4.3.0 h1:Y0aFCLWnU8ypRAF/wg8khMvo7LnMdIb8i3ePd+JXSZ0= -github.com/substrait-io/substrait-go/v4 v4.3.0/go.mod h1:GzpaFqO5VRtMkEjATgRxGK5p82OmEtCmszAVYxE+iWc= github.com/substrait-io/substrait-go/v4 v4.4.0 h1:mFArMNFxlOLyTuhPcaPzZCwYh6kUopTExTy7XOqtYBM= github.com/substrait-io/substrait-go/v4 v4.4.0/go.mod h1:GzpaFqO5VRtMkEjATgRxGK5p82OmEtCmszAVYxE+iWc= github.com/substrait-io/substrait-protobuf/go v0.71.0 h1:vkYGEEPJ8lWSwaJvX7Y+hEmwmrz5/qeDmGI43JpKJZE= @@ -1596,22 +1401,19 @@ github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tdewolff/minify/v2 v2.12.8 h1:Q2BqOTmlMjoutkuD/OPCnJUpIqrzT3nRPkw+q+KpXS0= github.com/tdewolff/minify/v2 v2.12.8/go.mod h1:YRgk7CC21LZnbuke2fmYnCTq+zhCgpb0yJACOTUNJ1E= github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8bBg= github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= -github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= -github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00= -github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0= -github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= -github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= github.com/testcontainers/testcontainers-go/modules/azurite v0.35.0 h1:gUZ25e1DVE/0+ZZ0nupsIo+C1j7UNloN7Pkg3w6tceI= github.com/testcontainers/testcontainers-go/modules/azurite v0.35.0/go.mod h1:2Fc67EpyOEexLAF99zhSuzu9H22zd83pkjxEHHTtHf4= github.com/testcontainers/testcontainers-go/modules/mongodb v0.35.0 h1:i1Kh9fmXgHG9z3uzJv5Arz7pDKVaaNpLrqyd+0xhYMA= github.com/testcontainers/testcontainers-go/modules/mongodb v0.35.0/go.mod h1:SD8nVMK1m7b/K2YJqYjYNzfHmZfqHtqNOlI44nfxjdg= github.com/testcontainers/testcontainers-go/modules/redis v0.35.0 h1:RBgVefU5j5IWapp3TNKqMTYX+M22OSjtuORjPd4+g08= github.com/testcontainers/testcontainers-go/modules/redis v0.35.0/go.mod h1:UgghVXQ0//D3MjC8X71Bpb/lUCChidjNCRILD+btqfU= +github.com/thanos-io/thanos v0.24.0/go.mod h1:sfnKJG7cDA41ixNL4gsTJEa3w9Qt8lwAjw+dqRMSDG0= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -1623,28 +1425,28 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= -github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= -github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= -github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= -github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= -github.com/tsenart/vegeta/v12 v12.12.0 h1:FKMMNomd3auAElO/TtbXzRFXAKGee6N/GKCGweFVm2U= -github.com/tsenart/vegeta/v12 v12.12.0/go.mod h1:gpdfR++WHV9/RZh4oux0f6lNPhsOH8pCjIGUlcPQe1M= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/twmb/franz-go v1.17.1/go.mod h1:NreRdJ2F7dziDY/m6VyspWd6sNxHKXdMZI42UfQ3GXM= github.com/twmb/franz-go v1.18.1 h1:D75xxCDyvTqBSiImFx2lkPduE39jz1vaD7+FNc+vMkc= github.com/twmb/franz-go v1.18.1/go.mod h1:Uzo77TarcLTUZeLuGq+9lNpSkfZI+JErv7YJhlDjs9M= github.com/twmb/franz-go/pkg/kadm v1.16.0 h1:STMs1t5lYR5mR974PSiwNzE5TvsosByTp+rKXLOhAjE= github.com/twmb/franz-go/pkg/kadm v1.16.0/go.mod h1:MUdcUtnf9ph4SFBLLA/XxE29rvLhWYLM9Ygb8dfSCvw= github.com/twmb/franz-go/pkg/kfake v0.0.0-20250320172111-35ab5e5f5327 h1:E2rCVOpwEnB6F0cUpwPNyzfRYfHee0IfHbUVSB5rH6I= github.com/twmb/franz-go/pkg/kfake v0.0.0-20250320172111-35ab5e5f5327/go.mod h1:zCgWGv7Rg9B70WV6T+tUbifRJnx60gGTFU/U4xZpyUA= +github.com/twmb/franz-go/pkg/kmsg v1.8.0/go.mod h1:HzYEb8G3uu5XevZbtU0dVbkphaKTHk0X68N5ka4q6mU= github.com/twmb/franz-go/pkg/kmsg v1.11.2 h1:hIw75FpwcAjgeyfIGFqivAvwC5uNIOWRGvQgZhH4mhg= github.com/twmb/franz-go/pkg/kmsg v1.11.2/go.mod h1:CFfkkLysDNmukPYhGzuUcDtf46gQSqCZHMW1T4Z+wDE= github.com/twmb/franz-go/plugin/kotel v1.6.0 h1:hmvLn/cVw/Hn56H3aJVJu/a/fh6m8J6Ajwp0IcEHbH8= github.com/twmb/franz-go/plugin/kotel v1.6.0/go.mod h1:ADmLuCa/NzHdXdWfl22FsIlGCack+YrHjivirHCBJaY= +github.com/twmb/franz-go/plugin/kprom v1.1.0/go.mod h1:cTDrPMSkyrO99LyGx3AtiwF9W6+THHjZrkDE2+TEBIU= github.com/twmb/franz-go/plugin/kprom v1.2.1 h1:FGWdneW9htySYmvJ5tEuAIZepjFOuTFhHLy5TrVR+QI= github.com/twmb/franz-go/plugin/kprom v1.2.1/go.mod h1:+dzpKnVE6By8BDRFj240dTDJS9bP2dngmuhv7egJ3Go= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= @@ -1653,32 +1455,37 @@ github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber/jaeger-client-go v2.28.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= -github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= +github.com/veraison/go-cose v1.0.0-rc.1/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw= github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/weaveworks/common v0.0.0-20220706100410-67d27ed40fae/go.mod h1:YfOOLoW1Q/jIIu0WLeSwgStmrKjuJEZSKTAUc+0KFvE= +github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA= github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA= github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= @@ -1693,20 +1500,16 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 h1:LY6cI8cP4B9rrpTleZk95+08kl2gF4rixG7+V/dwL6Q= github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 h1:ixAiqjj2S/dNuJqrz4AxSqgw2P5OBMXp68hB5nNriUk= github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1/go.mod h1:l5sSv153E18VvYcsmr51hok9Sjc16tEC8AXGbwrk+ho= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= @@ -1714,48 +1517,65 @@ 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.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ= go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg= -go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo= +go.etcd.io/etcd v3.3.25+incompatible h1:V1RzkZJj9LqsJRy+TUBgpWSbZXITLB819lstuTFoZOY= +go.etcd.io/etcd v3.3.25+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA= -go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8= +go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao= +go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/raft/v3 v3.5.21 h1:dOmE0mT55dIUsX77TKBLq+RgyumsQuYeiRQnW/ylugk= -go.etcd.io/etcd/raft/v3 v3.5.21/go.mod h1:fmcuY5R2SNkklU4+fKVBQi2biVp5vafMrWUEj4TJ4Cs= +go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI= go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA= go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= -go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= -go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opentelemetry.io/collector v0.121.0/go.mod h1:M4TlnmkjIgishm2DNCk9K3hMKTmAsY9w8cNFsp9EchM= go.opentelemetry.io/collector v0.124.0/go.mod h1:QzERYfmHUedawjr8Ph/CBEEkVqWS8IlxRLAZt+KHlCg= +go.opentelemetry.io/collector/client v1.29.0/go.mod h1:LCUoEV2KCTKA1i+/txZaGsSPVWUcqeOV6wCfNsAippE= go.opentelemetry.io/collector/client v1.30.0 h1:QbvOrvwUGcnVjnIBn2zyLLubisOjgh7kMgkzDAiYpHg= go.opentelemetry.io/collector/client v1.30.0/go.mod h1:msXhZlNdAra2fZiyeT0o/xj43Kl1yvF9zYW0r+FhGUI= +go.opentelemetry.io/collector/component v1.29.0/go.mod h1:qHoiz0iyBgyAYsT0F8FgtcJv8pTEQDvo1Ox3eO3qVlY= go.opentelemetry.io/collector/component v1.30.0 h1:HXjqBHaQ47/EEuWdnkjr4Y3kRWvmyWIDvqa1Q262Fls= go.opentelemetry.io/collector/component v1.30.0/go.mod h1:vfM9kN+BM6oHBXWibquiprz8CVawxd4/aYy3nbhme3E= +go.opentelemetry.io/collector/component/componentstatus v0.121.0/go.mod h1:ufRv8q15XNdbr9nNzdepMHlLl2aC3NHQgecCzp5VRns= go.opentelemetry.io/collector/component/componentstatus v0.124.0 h1:0WHaANNktxLIk+lN+CtgPBESI1MJBrfVW/LvNCbnMQ4= go.opentelemetry.io/collector/component/componentstatus v0.124.0/go.mod h1:a/wa8nxJGWOGuLwCN8gHCzFHCaUVZ+VyUYuKz9Yaq38= +go.opentelemetry.io/collector/component/componenttest v0.123.0/go.mod h1:4Y6EMvsgE9fUNM98G0eW5+LFXfcxdhTHQDhaOxJrgN8= go.opentelemetry.io/collector/component/componenttest v0.124.0 h1:Wsc+DmDrWTFs/aEyjDA3slNwV+h/0NOyIR5Aywvr6Zw= go.opentelemetry.io/collector/component/componenttest v0.124.0/go.mod h1:NQ4ATOzMFc7QA06B993tq8o27DR0cu/JR/zK7slGJ3E= +go.opentelemetry.io/collector/config/configauth v0.121.0/go.mod h1:jUjtq1xolk/w+J3fzbvPEak2sr07ZLFdLn0miJ5ACP4= go.opentelemetry.io/collector/config/configauth v0.124.0 h1:Qcu800axWnpX0xRfW+9Jyos9+GTR6m7gTIF1udEihEo= go.opentelemetry.io/collector/config/configauth v0.124.0/go.mod h1:Hz5PQnTvNk2yFp50rzf85H3k0MkdwEBdYUxhpRZn75E= +go.opentelemetry.io/collector/config/configcompression v1.27.0/go.mod h1:QwbNpaOl6Me+wd0EdFuEJg0Cc+WR42HNjJtdq4TwE6w= go.opentelemetry.io/collector/config/configcompression v1.30.0 h1:NKbywIEfL2PBiKnm9F2X2tbPNO0WzOQY08yWmndI3uM= go.opentelemetry.io/collector/config/configcompression v1.30.0/go.mod h1:QwbNpaOl6Me+wd0EdFuEJg0Cc+WR42HNjJtdq4TwE6w= +go.opentelemetry.io/collector/config/configgrpc v0.121.0/go.mod h1:NzsgaAUU5LemPl9aeYh8WWtLbaUAfkVD2uTSSWMmwyo= go.opentelemetry.io/collector/config/configgrpc v0.124.0 h1:aTuHYsyLMaGnd0o39Qy9KL1hZh2X8A1AlNR0YpB8vr0= go.opentelemetry.io/collector/config/configgrpc v0.124.0/go.mod h1:EEC4T+hCfXbbC+711GaVFuJNBU7hdYR4Vya4RWu7xc0= +go.opentelemetry.io/collector/config/confighttp v0.121.0/go.mod h1:SFv+5S9KFNDSe++ZFsJnyerXpcd0AAZ1FtOV/7mDdZU= go.opentelemetry.io/collector/config/confighttp v0.124.0 h1:W75DaPeLUuGbJtX3cTXOK0b53S5zrUsh6g5UfB6Wzsw= go.opentelemetry.io/collector/config/confighttp v0.124.0/go.mod h1:hiTu8HFgnzSitrogLz1urQn/+FzNzarqYk4BICy/ABs= go.opentelemetry.io/collector/config/confighttp/xconfighttp v0.121.0 h1:UpYVKYPvIxc7iekPYJ0xjN9jDFCdDIAkcT9WsRN6syg= go.opentelemetry.io/collector/config/confighttp/xconfighttp v0.121.0/go.mod h1:a5C/yjslp2GsNoA/OBBq7lYy2W3ie/drjNjfBtDgQlk= +go.opentelemetry.io/collector/config/confignet v1.27.0/go.mod h1:HgpLwdRLzPTwbjpUXR0Wdt6pAHuYzaIr8t4yECKrEvo= go.opentelemetry.io/collector/config/confignet v1.30.0 h1:2axBhT7xKpCUFgU6KokrDnG9cjtw7gXlACP1uVCUK0s= go.opentelemetry.io/collector/config/confignet v1.30.0/go.mod h1:HgpLwdRLzPTwbjpUXR0Wdt6pAHuYzaIr8t4yECKrEvo= +go.opentelemetry.io/collector/config/configopaque v1.27.0/go.mod h1:GYQiC8IejBcwE8z0O4DwbBR/Hf6U7d8DTf+cszyqwFs= go.opentelemetry.io/collector/config/configopaque v1.30.0 h1:vR2UxmzLwmkmQwyh16w8MyLODKdpNVKh0L3JFOZKzQ8= go.opentelemetry.io/collector/config/configopaque v1.30.0/go.mod h1:GYQiC8IejBcwE8z0O4DwbBR/Hf6U7d8DTf+cszyqwFs= +go.opentelemetry.io/collector/config/configretry v1.27.0/go.mod h1:8gzFQ0qzKLYvzP2sNPwsB9gwzKSEls649yANmt/d6yE= go.opentelemetry.io/collector/config/configretry v1.30.0 h1:sapni1tymwNiuI0PjqlRR5CvYxIQYT8tyjQGVJDkVPM= go.opentelemetry.io/collector/config/configretry v1.30.0/go.mod h1:QNnb+MCk7aS1k2EuGJMtlNCltzD7b8uC7Xel0Dxm1wQ= +go.opentelemetry.io/collector/config/configtelemetry v0.121.0/go.mod h1:WXmlNatI0vwjv7whh/qF1Xy+UufCZDk7VLtYqML7QmA= go.opentelemetry.io/collector/config/configtelemetry v0.124.0 h1:KIg5wlHKp8nI5g/hAWZug9fE5MlPZwkRP2ZHOi4I6FU= go.opentelemetry.io/collector/config/configtelemetry v0.124.0/go.mod h1:WXmlNatI0vwjv7whh/qF1Xy+UufCZDk7VLtYqML7QmA= +go.opentelemetry.io/collector/config/configtls v1.27.0/go.mod h1:i6kX7oboR1sO+J+hDImtKH4GnNCFiwcTAr2fzGRP0kI= go.opentelemetry.io/collector/config/configtls v1.30.0 h1:wLTRV5hn/FWKWNjZ/9/ckkeD2mqWzAtwzP1kQv1YZZE= go.opentelemetry.io/collector/config/configtls v1.30.0/go.mod h1:yCM4ZYkLvc1VjpT/1DQIVoGmzEBHOhZltYQ7A30BMyM= +go.opentelemetry.io/collector/confmap v1.27.0/go.mod h1:tmOa6iw3FJsEgfBHKALqvcdfRtf71JZGor0wSM5MoH8= go.opentelemetry.io/collector/confmap v1.30.0 h1:Y0MXhjQCdMyJN9xZMWWdNPWs6ncMVf7YVnyAEN2dAcM= go.opentelemetry.io/collector/confmap v1.30.0/go.mod h1:9DdThVDIC3VsdtTb7DgT+HwusWOocoqDkd/TErEtQgA= go.opentelemetry.io/collector/confmap/provider/envprovider v1.27.0 h1:zozJGcpzvv9DqgS9K8Ln5xQluPk2xUfvCVwJ4rifWqs= @@ -1768,74 +1588,104 @@ go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.27.0 h1:sfKbgYXa go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.27.0/go.mod h1:Nbpvr2DPGQewh9lC/qD0O+iOetDG3JWHqeKcYESCKrM= go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.27.0 h1:X7fohJKfGCS1cwY6/wmcjNmT/KGgcJfkyzBNnyNYiPQ= go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.27.0/go.mod h1:/vvb4mYN+J4IA2hRlkqAiBdxyOJFamW60ChxqDi8o1Y= +go.opentelemetry.io/collector/confmap/xconfmap v0.121.0/go.mod h1:YI1Sp8mbYro/H3rqH4csTq68VUuie5WVb7LI1o5+tVc= go.opentelemetry.io/collector/confmap/xconfmap v0.124.0 h1:PK+CaSgjLvzHaafBieJ3AjiUTAPuf40C+/Fn38LvmW8= go.opentelemetry.io/collector/confmap/xconfmap v0.124.0/go.mod h1:DZmFSgWiqXQrzld9uU+73YAVI5JRIgd8RkK5HcaXGU0= +go.opentelemetry.io/collector/connector v0.121.0/go.mod h1:njtHMkFOuZ5W5Ax2BnsqC8EThgTU7tF1k7OBpRs0+uQ= go.opentelemetry.io/collector/connector v0.124.0 h1:/Wk8A4gOqjhE+WvKCMqCFhzUIvSi3sdN3RGvopjD6SY= go.opentelemetry.io/collector/connector v0.124.0/go.mod h1:dnYcXgUZp8ZmT7nbBPf38+mP2DD3T47m9jyGbdaCEXc= +go.opentelemetry.io/collector/connector/connectortest v0.121.0/go.mod h1:Xu4oO85n1yi2+7RNq0MzjW9LYeW17mXn5qmTe1o/al8= go.opentelemetry.io/collector/connector/connectortest v0.124.0 h1:gAD2jt7Th6DD8tDTU72Sv2xXvqJEGSjfncr9nTSVCg8= go.opentelemetry.io/collector/connector/connectortest v0.124.0/go.mod h1:0017vT2aCY1NmYXEepxvEfMA9YufKUoBM3/qtD6k9UM= go.opentelemetry.io/collector/connector/forwardconnector v0.121.0 h1:AJtudkGK2eCp/jwoJAG5mN79+HOdthnZOUUynzUhzqc= go.opentelemetry.io/collector/connector/forwardconnector v0.121.0/go.mod h1:IntxKXr4nuDq0GjGsPpz+gO9J9ZIwJrDdHIZnpmkZGI= +go.opentelemetry.io/collector/connector/xconnector v0.121.0/go.mod h1:1Y/ypNTUkWEkm+nUP8mWKVMIRnQ/UPUuetp7RgnSfN0= go.opentelemetry.io/collector/connector/xconnector v0.124.0 h1:rdjwSfajHjJVRznw/NKGGzY0PKBTKBypZngGxOaJuEg= go.opentelemetry.io/collector/connector/xconnector v0.124.0/go.mod h1:rOhdUXPzTZbJ2L8VV43r7Rz/ZBfgWxQ+RI9mcqlzz5g= +go.opentelemetry.io/collector/consumer v1.29.0/go.mod h1:SRcOjDfuvTEk6pchJ0HuOBLsmbRG26+mZLrwXb9T3TQ= go.opentelemetry.io/collector/consumer v1.30.0 h1:Nn6kFTH+EJbv13E0W+sNvWrTgbiFCRv8f6DaA2F1DQs= go.opentelemetry.io/collector/consumer v1.30.0/go.mod h1:edRyfk61ugdhCQ93PBLRZfYMVWjdMPpKP8z5QLyESf0= +go.opentelemetry.io/collector/consumer/consumererror v0.123.0/go.mod h1:A9oAkn1emXnenyXiBb5iJNteWCidrM5remnzRqXHOKc= go.opentelemetry.io/collector/consumer/consumererror v0.124.0 h1:OmeJex0C8jcwyILG+eJIGDe6rGaR15fip+Rj3XyMTRY= go.opentelemetry.io/collector/consumer/consumererror v0.124.0/go.mod h1:A6WL/E70dUh9oFdQsL+aVYGL+d6UqiAhi7rIW/6gvDM= +go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.121.0/go.mod h1:TlLTSQSFbZGd+WYbyMdmRzRZhxGTVimkc6ZhfwEdxEg= go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.124.0 h1:OAZ65+hyjYSBLg7s7OdmGYwkStGHWR2743elhO9tX8A= go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.124.0/go.mod h1:/Mz+iI91QxtqbH9tgIHboipE1n5e8/j4ZSQvcK8D/hU= +go.opentelemetry.io/collector/consumer/consumertest v0.123.0/go.mod h1:MN6OZWsjF1JpvxEzGy7xFu0+PGtbzOASQnN6GgX7olA= go.opentelemetry.io/collector/consumer/consumertest v0.124.0 h1:2arChG4RPrHW3lfVWlK/KDF7Y7qkUm/YAiBXh8oTue0= go.opentelemetry.io/collector/consumer/consumertest v0.124.0/go.mod h1:Hlu+EXbINHxVAyIT1baKO2d0j5odR3fLlLAiaP+JqQg= +go.opentelemetry.io/collector/consumer/xconsumer v0.123.0/go.mod h1:+LAk3gs6kQ3O0+jLZhZAfgPEWUinxKkFqb3nZkYbC6w= go.opentelemetry.io/collector/consumer/xconsumer v0.124.0 h1:/cut96EWVNoz6lIeGI9+EzS6UClMtnZkx5YIpkD0Xe0= go.opentelemetry.io/collector/consumer/xconsumer v0.124.0/go.mod h1:fHH/MpzFCRNk/4foiYE6BoXQCAMf5sJTO35uvzVrrd4= +go.opentelemetry.io/collector/exporter v0.121.0/go.mod h1:Xi4UtotE9QdA7UGkJVxHyg4grycYIKVDp/F+henuv3k= go.opentelemetry.io/collector/exporter v0.124.0 h1:ii+9tU/iSrPl4+YDvqFVflksA9hUYEzwMIpmvP4JZ8w= go.opentelemetry.io/collector/exporter v0.124.0/go.mod h1:Q8tOEwFu3CN8VGjE4H2yZcCRG9Q60foQIyZGKPD/jig= go.opentelemetry.io/collector/exporter/debugexporter v0.121.0 h1:x9KJRTTEGPu7DxYNFC5LvP0KjrxCTS8uD6Zprn/gTWM= go.opentelemetry.io/collector/exporter/debugexporter v0.121.0/go.mod h1:90WFQRnuMryVAlQ9a5jIuh6yYsLg+K8P5uHG/z4Zyhg= +go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.121.0/go.mod h1:T9HNG2z0MPzfSJvDEFqgl0cc7gcZ50A7DHVbDVyGQR4= go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.124.0 h1:rU8CkyMIkLnjQAM6Yjd/a2gOQ/Svsjd/8jCNKEmrStw= go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.124.0/go.mod h1:2dO+z9QWGZwyxGOVLuCKAT0n+da4FMIaRjQDXdhcFyM= +go.opentelemetry.io/collector/exporter/exportertest v0.121.0/go.mod h1:xGIi17/Ffteh308BRruHXVzq51o7wxYcUch0zXzyrqA= go.opentelemetry.io/collector/exporter/exportertest v0.124.0 h1:IOxA/4CiVWGPlmA0JofK6W4DzvwW1YJes09r6osluIE= go.opentelemetry.io/collector/exporter/exportertest v0.124.0/go.mod h1:2EmU8IwVJV79MmFBFFW1LCN0Ob2UZsEkX/mSUB06lbI= go.opentelemetry.io/collector/exporter/nopexporter v0.121.0 h1:sgEtYYqzrqLtyhRr1afnmcOG3Il2i6soY7GdYhqrCgw= go.opentelemetry.io/collector/exporter/nopexporter v0.121.0/go.mod h1:KHLzB4ahpYu1YD4RPnhEArzh0L7/NtV1W2U4bzKH/Xo= +go.opentelemetry.io/collector/exporter/otlpexporter v0.121.0/go.mod h1:PPV7nVIWc9/cmItLt/bRUKrcdFY5bkpaUYXfAkXBhu4= go.opentelemetry.io/collector/exporter/otlpexporter v0.124.0 h1:wbOnCi01UT9YGkK86Jl13DEBTCdgIXSLmX8RajQ66LM= go.opentelemetry.io/collector/exporter/otlpexporter v0.124.0/go.mod h1:zzugHvRuxWsl8+T2Dj61QAfk7CSWhl26jp0y5LNPVig= +go.opentelemetry.io/collector/exporter/otlphttpexporter v0.121.0/go.mod h1:0Pu2pRoUfxGpggbtuGLo52mHb9F0NGPxG3Mja23KRPA= go.opentelemetry.io/collector/exporter/otlphttpexporter v0.124.0 h1:047D4wLOb5ug4O99y3AjR/SNFTxlq3RAr2zr/8G1JWI= go.opentelemetry.io/collector/exporter/otlphttpexporter v0.124.0/go.mod h1:Jk/1hiZvwy4dzFEfX37h18n3U9pNIfHSApcLjZmSFmU= +go.opentelemetry.io/collector/exporter/xexporter v0.121.0/go.mod h1:PGrW5pOQkNpaz8xqtkITCNEkcJVxOHSptg2chVTtv+o= go.opentelemetry.io/collector/exporter/xexporter v0.124.0 h1:Itfn2+F4ki8hObOtPCecWBwGpuxakUYSsTwwkB5iUns= go.opentelemetry.io/collector/exporter/xexporter v0.124.0/go.mod h1:dNK/PPY02gA9BawIKHyVk8kIFdYvqVZ2A+LlMZucIPY= +go.opentelemetry.io/collector/extension v1.27.0/go.mod h1:Fe0nUGMcr0c6IIBD3QEa3XmdUYpfmm5wCjc3PYho8DM= go.opentelemetry.io/collector/extension v1.30.0 h1:AJqntAp1p40Q1az2Vze3OHiMURq56KWnUxaLzs1ghaA= go.opentelemetry.io/collector/extension v1.30.0/go.mod h1:a21WpypFQp9x0Go7yMOknYmIKvdIoWGzjz+h1WMjzLk= +go.opentelemetry.io/collector/extension/extensionauth v0.121.0/go.mod h1:sINEH4b4YPSQJtvc/qcYTQdNRglDoKK0BUJqR+EHn94= go.opentelemetry.io/collector/extension/extensionauth v1.30.0 h1:HfNT4F1LDEyuItoHq01LrPiUmMpfc5LnOfE4OYVSghA= go.opentelemetry.io/collector/extension/extensionauth v1.30.0/go.mod h1:bVWkWyyd0aCYu+x6q4HdezfzL0QAqlq5PO7NwckXe4s= +go.opentelemetry.io/collector/extension/extensioncapabilities v0.121.0/go.mod h1:Wz7FanSxDq2aCbclOfqXTAm5CvWJCWeXzO6quzAGG04= go.opentelemetry.io/collector/extension/extensioncapabilities v0.124.0 h1:6emRXUQriceBcrwRDf2MPQQMRu7jmP0Z0XaJ4zdjt+I= go.opentelemetry.io/collector/extension/extensioncapabilities v0.124.0/go.mod h1:ovL3wgZuQ8/U7UxUBTqqGSYQTPxHPHmj4P+kp/zb0hA= +go.opentelemetry.io/collector/extension/extensiontest v0.121.0/go.mod h1:yrZhZhf2a3aD0/17drjHnzSTlr0XnNREVrOLYBlcP1o= go.opentelemetry.io/collector/extension/extensiontest v0.124.0 h1:pWfKxEqvq5vVdQy+UIerw5j7ZrXezxsfQfuUe8g/xVo= go.opentelemetry.io/collector/extension/extensiontest v0.124.0/go.mod h1:DLVRyW7tJt8TtYq0Wr5BUsM494YqDiIjN8YCmbVKqjs= +go.opentelemetry.io/collector/extension/xextension v0.121.0/go.mod h1:EiGx9nRD/7TU4++2/f5+2wdxUnDvjINCpWKLgfF2JRA= go.opentelemetry.io/collector/extension/xextension v0.124.0 h1:Yzf11HXaiMHfS50Zy/CYKfJjoi+/w/tgRZdDQ2VIdW0= go.opentelemetry.io/collector/extension/xextension v0.124.0/go.mod h1:GeM0aSgwVSba3Bvvspuy1E+1aa/Q1CDxoK+e/xcJFVg= go.opentelemetry.io/collector/extension/zpagesextension v0.121.0 h1:zCnIPyZwHkqq33MRROQN2JTKxpLadTq8ppiR1x9rbOM= go.opentelemetry.io/collector/extension/zpagesextension v0.121.0/go.mod h1:W2ZcPYdyN7ux7AD5fA0/YzW8EA2aOpUSia/YoDsh7ck= -go.opentelemetry.io/collector/featuregate v1.30.0 h1:mx7+iP/FQnY7KO8qw/xE3Qd1MQkWcU8VgcqLNrJ8EU8= go.opentelemetry.io/collector/featuregate v1.30.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc= +go.opentelemetry.io/collector/featuregate v1.43.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/internal/fanoutconsumer v0.121.0/go.mod h1:JM4FHRtacWUGEiZUpR2utoRfvgTdvYk+3OBJjyD9RzQ= go.opentelemetry.io/collector/internal/fanoutconsumer v0.124.0 h1:8+xc3OxriK1nZNBApFCzF7lszXyBQxyJ/Nnzy5Q4hCM= go.opentelemetry.io/collector/internal/fanoutconsumer v0.124.0/go.mod h1:CoT5fVYpTT4RWUE9DihSMlxXqGP/VnILnBBGld8Bu6o= go.opentelemetry.io/collector/internal/memorylimiter v0.121.0 h1:0QZGL5zwaHMAJqUVfyRtXNCJWhG47eyDBoRNSeQvEEc= go.opentelemetry.io/collector/internal/memorylimiter v0.121.0/go.mod h1:Be9ZYdHt0YibCEkw/W2v0jcEA8h3bLb/cmUW5gWg+QI= +go.opentelemetry.io/collector/internal/sharedcomponent v0.121.0/go.mod h1:tTiV58wJWHlcsLAXqfVUWzsYET5OBLsocvAe92Gwgso= go.opentelemetry.io/collector/internal/sharedcomponent v0.124.0 h1:HZTic8bbD86FFxE9xw8ZXLBCIwGObL9m70REo+64ZmY= go.opentelemetry.io/collector/internal/sharedcomponent v0.124.0/go.mod h1:yQs/QFRYvAS2ak2mh9CXblFGbFEPLJ7FaFc8rTpL8C0= +go.opentelemetry.io/collector/internal/telemetry v0.123.0/go.mod h1:Qwh5XcqAlAT9NorjaaSaNcilDgaecs8S+OKljJ9hFno= go.opentelemetry.io/collector/internal/telemetry v0.124.0 h1:kzd1/ZYhLj4bt2pDB529mL4rIRrRacemXodFNxfhdWk= go.opentelemetry.io/collector/internal/telemetry v0.124.0/go.mod h1:ZjXjqV0dJ+6D4XGhTOxg/WHjnhdmXsmwmUSgALea66Y= +go.opentelemetry.io/collector/model v0.46.0/go.mod h1:uyiyyq8lV45zrJ94MnLip26sorfNLP6J9XmOvaEmy7w= +go.opentelemetry.io/collector/otelcol v0.121.0/go.mod h1:+qxEAvnOUUUtZlow75IQsXKNeRzkmW80v2k2p5TlBn4= go.opentelemetry.io/collector/otelcol v0.124.0 h1:q/+ebTZgEZX+yFbvO7FeqpEtvtRPJ+YzZzHsVzqA71s= go.opentelemetry.io/collector/otelcol v0.124.0/go.mod h1:mFGJZn5YuffdMVO/lPBavbW+R64Dgd3jOMgw2WAmJEM= -go.opentelemetry.io/collector/pdata v1.30.0 h1:j3jyq9um436r6WzWySzexP2nLnFdmL5uVBYAlyr9nDM= go.opentelemetry.io/collector/pdata v1.30.0/go.mod h1:0Bxu1ktuj4wE7PIASNSvd0SdBscQ1PLtYasymJ13/Cs= +go.opentelemetry.io/collector/pdata v1.43.0/go.mod h1:KsJzdDG9e5BaHlmYr0sqdSEKeEiSfKzoF+rdWU7J//w= +go.opentelemetry.io/collector/pdata/pprofile v0.123.0/go.mod h1:af2WDdbCc+JA+FokOnsW3l+agsnW5ed8LkLn6CfneII= +go.opentelemetry.io/collector/pdata/testdata v0.121.0/go.mod h1:UhiSwmVpBbuKlPdmhBytiVTHipSz/JO6c4mbD4kWOPg= go.opentelemetry.io/collector/pdata/testdata v0.124.0 h1:vY+pWG7CQfzzGSB5+zGYHQOltRQr59Ek9QiPe+rI+NY= go.opentelemetry.io/collector/pdata/testdata v0.124.0/go.mod h1:lNH48lGhGv4CYk27fJecpsR1zYHmZjKgNrAprwjym0o= +go.opentelemetry.io/collector/pipeline v0.123.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= go.opentelemetry.io/collector/pipeline v0.124.0 h1:hKvhDyH2GPnNO8LGL34ugf36sY7EOXPjBvlrvBhsOdw= go.opentelemetry.io/collector/pipeline v0.124.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= +go.opentelemetry.io/collector/pipeline/xpipeline v0.121.0/go.mod h1:nTfAnIPgIwevodUp9z0gwfl2S+lVEvz3CjhOqU/Lk/8= go.opentelemetry.io/collector/pipeline/xpipeline v0.124.0 h1:ADHUrozlIgSDjXMsAC5t8l4p9TVo+QH33XArFfcL9ns= go.opentelemetry.io/collector/pipeline/xpipeline v0.124.0/go.mod h1:ep7XJFdCEq04/5yUyiWWzgKvBYMwRJR5XNWmGpIGbVQ= +go.opentelemetry.io/collector/processor v0.121.0/go.mod h1:BoFEMvPn5/p53eWz+R9cibIxCXzaRZ/RtcBPtvqXNaQ= go.opentelemetry.io/collector/processor v1.30.0 h1:dxmu+sO6MzQydyrf2CON5Hm1KU7yV4ofH1stmreUtPk= go.opentelemetry.io/collector/processor v1.30.0/go.mod h1:DjXAgelT8rfIWCTJP5kiPpxPqz4JLE1mJwsE2kJMTk8= go.opentelemetry.io/collector/processor/batchprocessor v0.121.0 h1:dKE9lMhaQgg0VIU3/cVKPsPz8K8JXpE752C+iFDop8k= @@ -1846,129 +1696,149 @@ go.opentelemetry.io/collector/processor/processorhelper v0.124.0 h1:zIBpPn/88FVu go.opentelemetry.io/collector/processor/processorhelper v0.124.0/go.mod h1:NNy1C10OSGSV9nUl+YXYBETYxNacGPYM1PHFb+j0Su4= go.opentelemetry.io/collector/processor/processorhelper/xprocessorhelper v0.121.0 h1:O4CzvJCV1soQOoHSew+FEGhbhXWPxJGB7pYYkAhdEUU= go.opentelemetry.io/collector/processor/processorhelper/xprocessorhelper v0.121.0/go.mod h1:CUuVIwN4uAJzuAEr3hYaQXmc865eS37eJl5jQ2LgbWc= +go.opentelemetry.io/collector/processor/processortest v0.121.0/go.mod h1:oL4S/eguZ6XTK6IxAQXhXD9yWuRrG5/Maiskbf9HL0o= go.opentelemetry.io/collector/processor/processortest v0.124.0 h1:qcyo0dSWmgpNFxjObsKk3Rd/wWV8CkMevd+jApkTQWE= go.opentelemetry.io/collector/processor/processortest v0.124.0/go.mod h1:1YDTxd4c/uVU3Ui1+AzvYW94mo5DbhNmB1xSof6zvD0= +go.opentelemetry.io/collector/processor/xprocessor v0.121.0/go.mod h1:Puk+6YYKyqLVKqpftUXg0blMrd3BlH/Av+oiajp1sHQ= go.opentelemetry.io/collector/processor/xprocessor v0.124.0 h1:KAe8gIje8TcB8varZ4PDy0HV5xX5rNdaQ7q46BE915w= go.opentelemetry.io/collector/processor/xprocessor v0.124.0/go.mod h1:ItJBBlR6/141vg1v4iRrcsBrGjPCgmXAztxS2x2YkdI= +go.opentelemetry.io/collector/receiver v1.29.0/go.mod h1:4QoZQZl4r9bIrKa7VLRw3Rp7++ezc5QoXn9VoPZwfoI= go.opentelemetry.io/collector/receiver v1.30.0 h1:XbgU4yT3Ld+hL9+jHcD/Kctcr3gXjpiFxKO+50pSayg= go.opentelemetry.io/collector/receiver v1.30.0/go.mod h1:U3cApz9PHiRMgN0WkZaz4o8mvj1+cVQYsyj2Nl1v3FQ= go.opentelemetry.io/collector/receiver/nopreceiver v0.121.0 h1:fAlBC6JXyKnd6OY0oK0pk1ROc/VfA0OonrPOixXBZqQ= go.opentelemetry.io/collector/receiver/nopreceiver v0.121.0/go.mod h1:iyAp4FFiNCkERuFnFhHNbhNiVrk04pEZJQTEw5KLMMA= +go.opentelemetry.io/collector/receiver/otlpreceiver v0.121.0/go.mod h1:dt9HG89hdBxpT7Bsr8GnSwH3uIRh1FdrYIcFgtmg1SA= go.opentelemetry.io/collector/receiver/otlpreceiver v0.124.0 h1:DwvPyh6X0ytmD0IIjOq62ui3Bg5FwkaX6DzseGg62sY= go.opentelemetry.io/collector/receiver/otlpreceiver v0.124.0/go.mod h1:QG2c9RW/Ys1L30cGH24RBJk72H27UdB50YmpY2rclNs= go.opentelemetry.io/collector/receiver/receiverhelper v0.124.0 h1:lOCWpiZUzeXV0zDN4jzwqgD6gFdhsY9Mp1om5XzrWKk= go.opentelemetry.io/collector/receiver/receiverhelper v0.124.0/go.mod h1:JYgWF/EhiDrhq4C3//GTsppsWjWBNwnYPcbKiJX80qU= +go.opentelemetry.io/collector/receiver/receivertest v0.123.0/go.mod h1:eDcm8N4yFz//V5dSa6MeEdjuK5Nq1X+j9xP9kPJr8BU= go.opentelemetry.io/collector/receiver/receivertest v0.124.0 h1:mx0290aXAo+wfjm4NgbKUodjT5SbS306zmk+AeqeVxE= go.opentelemetry.io/collector/receiver/receivertest v0.124.0/go.mod h1:3RpopRmIzx5T4zTStHJC0HHfd8YFWm8e9bia1HiuDtY= +go.opentelemetry.io/collector/receiver/xreceiver v0.123.0/go.mod h1:M3a0ACBOPHag90EV+GbIWKZhJkHXW7Ka2xabBGY25aw= go.opentelemetry.io/collector/receiver/xreceiver v0.124.0 h1:YigTUKk8p/aIfqaT0ST7teT9KbLThWD5n2km83byftw= go.opentelemetry.io/collector/receiver/xreceiver v0.124.0/go.mod h1:NkTpmpAEDT17Dko4gpHUnRztrSkdSd6B0+Y4gfuCWIA= +go.opentelemetry.io/collector/service v0.121.0/go.mod h1:39g0114ia8bxz/MnlJk12DvAJuIQKijFJg7PdD0ydQw= go.opentelemetry.io/collector/service v0.124.0 h1:lUpizko/Y2P+XXbZ9wiKM8acLSt6ZIvC3/6/j6rcq4w= go.opentelemetry.io/collector/service v0.124.0/go.mod h1:w2eL3KKOMW4CvqCWyZ3P/Qh1ZBEPGG/uRz/0LpHbpv0= +go.opentelemetry.io/collector/service/hostcapabilities v0.121.0/go.mod h1:yjiV184ZrMoZwhbNuS7arbqo8NokJR6o93zlhEYd7ow= go.opentelemetry.io/collector/service/hostcapabilities v0.124.0 h1:ArxbARF7+bnzK8xLnN2G41KInbcN1aGhSBR76VeUQi8= go.opentelemetry.io/collector/service/hostcapabilities v0.124.0/go.mod h1:vifQsB+lkeCsjBCRPVHca9lJ3pLpLPZKCGrG77nkxFQ= go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU= go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0= go.opentelemetry.io/contrib/config v0.14.0 h1:QAG8uHNp5ZiCkpT7XggSmg5AyW1sA0LgypMoXgBB1+4= go.opentelemetry.io/contrib/config v0.14.0/go.mod h1:77rDmFPqBae5jtQ2C78RuDTHz4P27C8LzoN0MZyumYQ= -go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/detectors/aws/ec2 v1.37.0 h1:BJnWw8+FULhuuF/6R6B/JYqAlCTCy9E4J8qmLpo/7KU= +go.opentelemetry.io/contrib/detectors/aws/ec2 v1.37.0/go.mod h1:gs3y8jvJscW5D+FzrZvJZEsGj+xlMCF0S1x4R6ktiNo= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.59.0 h1:I8k9HW4yl8SRYNmECKKtjhcOvq9lAP9riqYPixBU3qw= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.59.0/go.mod h1:/vTiuiSKBQAerQeMB3CsVJbXd+cvTbhcdOk5AV5Z5R0= -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.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0/go.mod h1:vsh3ySueQCiKPxFLvjWC4Z135gIa34TQ/NSqkDTZYUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0/go.mod h1:HfvuU0kW9HewH14VCOLImqKvUgONodURG7Alj/IrnGI= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.62.0/go.mod h1:WfEApdZDMlLUAev/0QQpr8EJ/z0VWDKYZ5tF5RH5T1U= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= 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.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 h1:ZIt0ya9/y4WyRIzfLC8hQRRsWg0J9M9GyaGtIMiElZI= -go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0/go.mod h1:F1aJ9VuiKWOlWwKdTYDUp1aoS0HzQxg38/VLxKmhm5U= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/contrib/otelconf v0.15.0 h1:BLNiIUsrNcqhSKpsa6CnhE6LdrpY1A8X0szMVsu99eo= go.opentelemetry.io/contrib/otelconf v0.15.0/go.mod h1:OPH1seO5z9dp1P26gnLtoM9ht7JDvh3Ws6XRHuXqImY= +go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss= +go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0= +go.opentelemetry.io/contrib/propagators/b3 v1.34.0/go.mod h1:FwM71WS8i1/mAK4n48t0KU6qUS/OZRBgDrHZv3RlJ+w= go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ= go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg= go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= +go.opentelemetry.io/contrib/propagators/jaeger v1.36.0/go.mod h1:VHu48l0YTRKSObdPQ+Sb8xMZvdnJlN7yhHuHoPgNqHM= go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.30.0/go.mod h1:9b8Q9rH52NgYH3ShiTFB5wf18Vt3RTH/VMB7LDcC1ug= go.opentelemetry.io/contrib/samplers/jaegerremote v0.31.0/go.mod h1:XAOSk4bqj5vtoiY08bexeiafzxdXeLlxKFnwscvn8Fc= go.opentelemetry.io/contrib/zpages v0.60.0 h1:wOM9ie1Hz4H88L9KE6GrGbKJhfm+8F1NfW/Y3q9Xt+8= go.opentelemetry.io/contrib/zpages v0.60.0/go.mod h1:xqfToSRGh2MYUsfyErNz8jnNDPlnpZqWM/y6Z2Cx7xw= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/bridge/opencensus v0.25.0/go.mod h1:dkZDdaNwLlIutxK2Kc2m3jwW2M1ISaNf8/rOYVwuVHs= go.opentelemetry.io/otel/bridge/opencensus v1.35.0 h1:4nJfffRbozhqnuukfRkiahA94mnpryCLJLiduMIDJKI= go.opentelemetry.io/otel/bridge/opencensus v1.35.0/go.mod h1:359S30saRYNsB4A46EDx91SpXsQFNgkma7ftg2/L5/M= +go.opentelemetry.io/otel/bridge/opentracing v1.2.0/go.mod h1:EyVJNmSj/3xsOQxezXM58bmoiv+ZOGKVcInF9TZGXCg= go.opentelemetry.io/otel/bridge/opentracing v1.35.0 h1:qT4jl1fYl0hHuRopNcwS94QosLFhGYcS0HacPUeXmT4= go.opentelemetry.io/otel/bridge/opentracing v1.35.0/go.mod h1:p5CbIL4v7uQz7mnQD6T/AZc1pPUzwz+2wZ1zrGY9Kgs= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= +go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/export/metric v0.25.0/go.mod h1:Ej7NOa+WpN49EIcr1HMUYRvxXXCCnQCg2+ovdt2z8Pk= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +gocloud.dev v0.42.0/go.mod h1:zkaYAapZfQisXOA4bzhsbA4ckiStGQ3Psvs9/OQ5dPM= +gocloud.dev/secrets/hashivault v0.42.0/go.mod h1:LXprr1XLEAT7BVZ+Y66dJEHQMzDsowIExj5Ktr9HLvM= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= @@ -1976,8 +1846,8 @@ golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= @@ -1988,20 +1858,22 @@ golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhp golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= @@ -2010,32 +1882,37 @@ golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -2044,19 +1921,16 @@ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -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/telemetry v0.0.0-20250710130107-8d8967aff50b h1:DU+gwOBXU+6bO0sEyO7o/NeMlxZxCZEvI7v+J4a1zRQ= -golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= -golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488 h1:3doPGa+Gg4snce233aCWnbZVFsyFMo/dR40KK/6skyE= -golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= @@ -2064,28 +1938,30 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.24.1/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= @@ -2094,81 +1970,50 @@ golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGN gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.15.2 h1:Tlfh/jBk2tqjLZ4/P8ZIwGrLEWQSPDLRm/SNWKNXiGI= gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= -google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= -google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0= -google.golang.org/api v0.214.0/go.mod h1:bYPpLG8AyeMWwDU6NXoB00xC0DFkikVvd5MfwoxjLqE= -google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY= -google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= -google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M= -google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= -google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= -google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0= -google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= +google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I= +google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422/go.mod h1:1NPAxoesyw/SgLPqaUp9u1f9PWCLAk/jVmhx7gJZStg= -google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:CCviP9RmpZ1mxVr8MUjCnSiY09IbAXZxhLE6EhHIdPU= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw= -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-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU= -google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= -google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A= -google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ= google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE= google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs= google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20250505200425-f936aa4a68b2 h1:DbpkGFGRkd4GORg+IWQW2EhxUaa/My/PM8d1CGyTDMY= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b736a9 h1:YI36gCL8AQMhzYN6+jH8PdV/iZ0On+Zd0rO/7lCH3k8= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -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-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= @@ -2176,45 +2021,41 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= @@ -2222,77 +2063,62 @@ gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= -gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= -gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= -howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= +k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= +k8s.io/apiserver v0.26.2/go.mod h1:GHcozwXgXsPuOJ28EnQ/jXEM9QeG6HT22YxSNmpYNh8= +k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= +k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= -k8s.io/code-generator v0.33.1 h1:ZLzIRdMsh3Myfnx9BaooX6iQry29UJjVfVG+BuS+UMw= -k8s.io/code-generator v0.33.1/go.mod h1:HUKT7Ubp6bOgIbbaPIs9lpd2Q02uqkMCMx9/GjDrWpY= -k8s.io/code-generator v0.33.2 h1:PCJ0Y6viTCxxJHMOyGqYwWEteM4q6y1Hqo2rNpl6jF4= -k8s.io/code-generator v0.33.2/go.mod h1:hBjCA9kPMpjLWwxcr75ReaQfFXY8u+9bEJJ7kRw3J8c= -k8s.io/code-generator v0.33.3 h1:6+34LhYkIuQ/yn/E3qlpVqjQaP8smzCu4NE1A8b0LWs= -k8s.io/code-generator v0.33.3/go.mod h1:6Y02+HQJYgNphv9z3wJB5w+sjYDIEBQW7sh62PkufvA= k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc= k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg= -k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +k8s.io/component-base v0.26.2/go.mod h1:DxbuIe9M3IZPRxPIzhch2m1eT7uFrSBJUBuVCQEBivs= +k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= +k8s.io/cri-api v0.27.1/go.mod h1:+Ts/AVYbIo04S86XbTD73UPp/DkTiYxtsFeOFEu32L0= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk= -k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= -k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog= -k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kms v0.34.0/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= -k8s.io/kube-aggregator v0.33.1/go.mod h1:16/wlU5Lj7hNJSv7JSu5FLvxyrgiJVLCHzfVoECAsuI= -k8s.io/kube-aggregator v0.33.2/go.mod h1:qQbliLwcdmx7/8mtvkc/9QV/ON2M6ZBMcffEUmrqKFw= -k8s.io/kube-aggregator v0.33.3/go.mod h1:hwvkUoQ8q6gv0+SgNnlmQ3eUue1zHhJKTHsX7BwxwSE= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -knative.dev/hack v0.0.0-20250514121446-f525e187efdc h1:8HmclJlA0zNE/G1SkgdC3/IFSSyhaSz2iIhihU6YbEo= -knative.dev/hack v0.0.0-20250514121446-f525e187efdc/go.mod h1:R0ritgYtjLDO9527h5vb5X6gfvt5LCrJ55BNbVDsWiY= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg= -modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= -modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM= -modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= -modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= -modernc.org/ccorpus2 v1.5.2 h1:Ui+4tc58mf/W+2arcYCJR903y3zl3ecsI7Fpaaqozyw= -modernc.org/ccorpus2 v1.5.2/go.mod h1:Wifvo4Q/qS/h1aRoC2TffcHsnxwTikmi1AuLANuucJQ= -modernc.org/ccorpus2 v1.5.4 h1:k9A52f3NsUQzHStOav5ukvkdKz63CW5po4gaC5VH4Qc= modernc.org/ccorpus2 v1.5.4/go.mod h1:Wifvo4Q/qS/h1aRoC2TffcHsnxwTikmi1AuLANuucJQ= -modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= -modernc.org/lex v1.1.1 h1:prSCNTLw1R4rn7M/RzwsuMtAuOytfyR3cnyM07P+Pas= modernc.org/lex v1.1.1/go.mod h1:6r8o8DLJkAnOsQaGi8fMoi+Vt6LTbDaCrkUK729D8xM= -modernc.org/lexer v1.0.4 h1:hU7xVbZsqwPphyzChc7nMSGrsuaD2PDNOmzrzkS5AlE= modernc.org/lexer v1.0.4/go.mod h1:tOajb8S4sdfOYitzCgXDFmbVJ/LE0v1fNJ7annTw36U= modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/scannertest v1.0.2 h1:JPtfxcVdbRvzmRf2YUvsDibJsQRw8vKA/3jb31y7cy0= +modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/scannertest v1.0.2/go.mod h1:RzTm5RwglF/6shsKoEivo8N91nQIoWtcWI7ns+zPyGA= modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= @@ -2301,11 +2127,19 @@ rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= +tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto= +tags.cncf.io/container-device-interface/specs-go v0.7.0/go.mod h1:hMAwAbMZyBLdmYqWgYcKH0F/yctNpV3P35f+/088A80= +xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= diff --git a/i18next.config.ts b/i18next.config.ts new file mode 100644 index 00000000000..0098d5e8784 --- /dev/null +++ b/i18next.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'i18next-cli'; + +export default defineConfig({ + locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages + extract: { + ignore: ['public/lib/monaco/**/*', 'public/app/extensions/**/*', 'public/app/plugins/datasource/**/*'], + input: ['public/**/*.{tsx,ts}', 'packages/grafana-ui/**/*.{tsx,ts}', 'packages/grafana-data/**/*.{tsx,ts}'], + output: 'public/locales/{{language}}/{{namespace}}.json', + defaultNS: 'grafana', + functions: ['t', '*.t'], + transComponents: ['Trans'], + }, +}); diff --git a/jest.config.codeowner.js b/jest.config.codeowner.js index 5e1a2755949..8e26692a8d4 100644 --- a/jest.config.codeowner.js +++ b/jest.config.codeowner.js @@ -35,17 +35,20 @@ const sourceFiles = teamFiles.filter((file) => { const ext = path.extname(file); return ( ['.ts', '.tsx', '.js', '.jsx'].includes(ext) && - // exclude all tests + // exclude all tests and mocks !path.matchesGlob(file, '**/test/**/*') && !file.includes('.test.') && !file.includes('.spec.') && + !path.matchesGlob(file, '**/__mocks__/**/*') && // and storybook stories !file.includes('.story.') && // and generated files !file.includes('.gen.ts') && // and type definitions !file.includes('.d.ts') && - !file.endsWith('/types.ts') + !file.endsWith('/types.ts') && + // and anything in graveyard + !path.matchesGlob(file, '**/graveyard/**/*') ); }); diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 13c2b4f25f6..ce2510d2cfe 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -173,6 +173,9 @@ lineage: schemas: [{ // Set to 1 for the standard annotation query all dashboards have by default. builtIn?: number | *0 + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: #AnnotationQueryPlacement + // unless datasources have migrated to the target+mapping, // they just spread their query into the base object :( ... @@ -298,6 +301,10 @@ lineage: schemas: [{ // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") + // Annotation Query placement. Defines where the annotation query should be displayed. + // - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu + #AnnotationQueryPlacement: "inControlsMenu" @cuetsy(kind="type") + // Dashboard action type #ActionType: "fetch" | "infinity" @cuetsy(kind="type") @@ -666,6 +673,10 @@ lineage: schemas: [{ // Controls if the timeFrom or timeShift overrides are shown in the panel header hideTimeOverride?: bool + // Compare the current time range with a previous period + // For example "1d" to compare current period but shifted back 1 day + timeCompare?: string + // Dynamically load the panel libraryPanel?: #LibraryPanelRef diff --git a/lerna.json b/lerna.json index 70b729cb019..d6dc835d802 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "npmClient": "yarn", - "version": "12.3.0-pre" + "version": "12.4.0-pre" } diff --git a/package.json b/package.json index df96a8b37a2..f626ebc1486 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "repository": "github:grafana/grafana", "scripts": { "check-frontend-dev": "./scripts/check-frontend-dev.sh", @@ -92,11 +92,11 @@ "@emotion/eslint-plugin": "11.12.0", "@grafana/eslint-config": "8.2.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "2.1.7", + "@grafana/plugin-e2e": "^3.0.1", "@grafana/test-utils": "workspace:*", "@manypkg/get-packages": "^3.0.0", "@npmcli/package-json": "^6.0.0", - "@playwright/test": "1.54.1", + "@playwright/test": "1.56.1", "@pmmmwh/react-refresh-webpack-plugin": "0.6.1", "@react-types/button": "3.13.0", "@react-types/menu": "3.10.3", @@ -136,7 +136,7 @@ "@types/lodash": "4.17.20", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.3.0", "@types/pluralize": "^0.0.33", @@ -210,7 +210,7 @@ "html-loader": "5.1.0", "html-webpack-plugin": "5.6.3", "http-server": "14.1.1", - "i18next-parser": "9.3.0", + "i18next-cli": "1.11.12", "ini": "^5.0.0", "jest": "29.7.0", "jest-canvas-mock": "2.5.2", @@ -278,9 +278,9 @@ "@glideapps/glide-data-grid": "^6.0.0", "@grafana/alerting": "workspace:*", "@grafana/api-clients": "workspace:*", - "@grafana/assistant": "0.1.0", + "@grafana/assistant": "0.1.1", "@grafana/aws-sdk": "0.7.1", - "@grafana/azure-sdk": "0.0.7", + "@grafana/azure-sdk": "0.0.8", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", "@grafana/faro-core": "^1.19.0", @@ -296,8 +296,8 @@ "@grafana/plugin-ui": "^0.10.10", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^6.39.8", - "@grafana/scenes-react": "^6.39.8", + "@grafana/scenes": "^6.42.1", + "@grafana/scenes-react": "^6.42.1", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", @@ -383,6 +383,7 @@ "node-forge": "^1.3.1", "ol": "10.6.1", "ol-ext": "4.0.35", + "ol-mapbox-style": "^13.0.1", "pluralize": "^8.0.0", "prismjs": "1.30.0", "rc-slider": "11.1.9", diff --git a/packages/grafana-alerting/i18next.config.ts b/packages/grafana-alerting/i18next.config.ts new file mode 100644 index 00000000000..0663f4a043e --- /dev/null +++ b/packages/grafana-alerting/i18next.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'i18next-cli'; + +export default defineConfig({ + locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages + extract: { + input: ['src/**/*.{tsx,ts}'], + output: 'src/locales/{{language}}/{{namespace}}.json', + defaultNS: 'grafana-alerting', + functions: ['t', '*.t'], + transComponents: ['Trans'], + }, +}); diff --git a/packages/grafana-alerting/package.json b/packages/grafana-alerting/package.json index 167358447d0..da13aa6fc0f 100644 --- a/packages/grafana-alerting/package.json +++ b/packages/grafana-alerting/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/alerting", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Grafana Alerting Library – Build vertical integrations on top of the industry-leading alerting solution", "keywords": [ "typescript", @@ -59,7 +59,7 @@ "codegen": "rtk-query-codegen-openapi ./scripts/codegen.ts", "prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=testing,unstable node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json && rimraf ./unstable ./testing", - "i18n-extract": "i18next --config src/locales/i18next-parser.config.cjs" + "i18n-extract": "i18next-cli extract --sync-primary" }, "devDependencies": { "@grafana/test-utils": "workspace:*", @@ -72,6 +72,7 @@ "@types/react-dom": "18.3.5", "@types/tinycolor2": "^1", "i18next": "^25.5.2", + "i18next-cli": "1.11.12", "react": "18.3.1", "react-dom": "18.3.1", "react-redux": "^9.2.0", @@ -93,7 +94,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@faker-js/faker": "^9.8.0", - "@grafana/i18n": "12.3.0-pre", + "@grafana/i18n": "12.4.0-pre", "@reduxjs/toolkit": "^2.9.0", "fishery": "^2.3.1", "lodash": "^4.17.21", diff --git a/packages/grafana-alerting/src/grafana/rules/components/labels/AlertLabels.tsx b/packages/grafana-alerting/src/grafana/rules/components/labels/AlertLabels.tsx index 3646c02a13e..b2bd9fee77d 100644 --- a/packages/grafana-alerting/src/grafana/rules/components/labels/AlertLabels.tsx +++ b/packages/grafana-alerting/src/grafana/rules/components/labels/AlertLabels.tsx @@ -1,10 +1,10 @@ import { css } from '@emotion/css'; import { chain } from 'lodash'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Button, useStyles2 } from '@grafana/ui'; +import { Button, Stack, Toggletip, useStyles2 } from '@grafana/ui'; import { findCommonLabels, isPrivateLabel } from '../../utils/labels'; @@ -16,14 +16,24 @@ export interface AlertLabelsProps { labelSets?: Array>; size?: LabelSize; onClick?: ([value, key]: [string | undefined, string | undefined]) => void; + commonLabelsMode?: 'expand' | 'tooltip'; } -export const AlertLabels = ({ labels, displayCommonLabels, labelSets, size, onClick }: AlertLabelsProps) => { +export const AlertLabels = ({ + labels, + displayCommonLabels, + labelSets, + size, + onClick, + commonLabelsMode = 'expand', +}: AlertLabelsProps) => { const styles = useStyles2(getStyles, size); const [showCommonLabels, setShowCommonLabels] = useState(false); - const computedCommonLabels = - displayCommonLabels && Array.isArray(labelSets) && labelSets.length > 1 ? findCommonLabels(labelSets) : {}; + const computedCommonLabels = useMemo( + () => (displayCommonLabels && Array.isArray(labelSets) && labelSets.length > 1 ? findCommonLabels(labelSets) : {}), + [displayCommonLabels, labelSets] + ); const labelsToShow = chain(labels) .toPairs() @@ -35,6 +45,17 @@ export const AlertLabels = ({ labels, displayCommonLabels, labelSets, size, onCl const hasCommonLabels = commonLabelsCount > 0; const tooltip = t('alert-labels.button.show.tooltip', 'Show common labels'); + const commonLabelsTooltip = useMemo( + () => ( + + {Object.entries(computedCommonLabels).map(([label, value]) => ( + + ))} + + ), + [computedCommonLabels, size] + ); + return (
{labelsToShow.map(([label, value]) => { @@ -53,18 +74,28 @@ export const AlertLabels = ({ labels, displayCommonLabels, labelSets, size, onCl {!showCommonLabels && hasCommonLabels && (
- + {commonLabelsMode === 'expand' ? ( + + ) : ( + + + + )}
)} {showCommonLabels && hasCommonLabels && ( diff --git a/packages/grafana-alerting/src/grafana/rules/components/state/types.ts b/packages/grafana-alerting/src/grafana/rules/components/state/types.ts index c37ff0851f4..65e2c165f2f 100644 --- a/packages/grafana-alerting/src/grafana/rules/components/state/types.ts +++ b/packages/grafana-alerting/src/grafana/rules/components/state/types.ts @@ -1,3 +1,3 @@ -export type Health = 'nodata' | 'error'; +export type Health = 'ok' | 'nodata' | 'error'; export type State = 'normal' | 'firing' | 'pending' | 'unknown' | 'recovering'; export type Type = 'alerting' | 'recording'; diff --git a/packages/grafana-alerting/src/locales/i18next-parser.config.cjs b/packages/grafana-alerting/src/locales/i18next-parser.config.cjs deleted file mode 100644 index 47939b46cc9..00000000000 --- a/packages/grafana-alerting/src/locales/i18next-parser.config.cjs +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages - sort: true, - createOldCatalogs: false, - failOnWarnings: true, - verbose: false, - resetDefaultValueLocale: 'en-US', // Updates extracted values when they change in code - - defaultNamespace: 'grafana-alerting', - input: ['../**/*.{tsx,ts}'], - output: './src/locales/$LOCALE/$NAMESPACE.json', -}; diff --git a/packages/grafana-api-clients/package.json b/packages/grafana-api-clients/package.json index f15a198f13e..2240a34eccb 100644 --- a/packages/grafana-api-clients/package.json +++ b/packages/grafana-api-clients/package.json @@ -3,7 +3,7 @@ "license": "Apache-2.0", "private": true, "name": "@grafana/api-clients", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Grafana API client utilities", "keywords": [ "grafana", @@ -48,6 +48,10 @@ "import": "./src/clients/rtkq/iam/v0alpha1/index.ts", "require": "./src/clients/rtkq/iam/v0alpha1/index.ts" }, + "./rtkq/legacy": { + "import": "./src/clients/rtkq/legacy/index.ts", + "require": "./src/clients/rtkq/legacy/index.ts" + }, "./rtkq/legacy/migrate-to-cloud": { "import": "./src/clients/rtkq/migrate-to-cloud/index.ts", "require": "./src/clients/rtkq/migrate-to-cloud/index.ts" @@ -71,14 +75,6 @@ "./rtkq/provisioning/v0alpha1": { "import": "./src/clients/rtkq/provisioning/v0alpha1/index.ts", "require": "./src/clients/rtkq/provisioning/v0alpha1/index.ts" - }, - "./rtkq/shorturl/v1alpha1": { - "import": "./src/clients/rtkq/shorturl/v1alpha1/index.ts", - "require": "./src/clients/rtkq/shorturl/v1alpha1/index.ts" - }, - "./rtkq/correlations/v0alpha1": { - "import": "./src/clients/rtkq/correlations/v0alpha1/index.ts", - "require": "./src/clients/rtkq/correlations/v0alpha1/index.ts" } }, "publishConfig": { diff --git a/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts index d054b90a9f4..3b0fb2a5134 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts @@ -1,11 +1,15 @@ import { api } from './baseAPI'; -export const addTagTypes = ['Check', 'CheckType'] as const; +export const addTagTypes = ['API Discovery', 'Check', 'CheckType'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ + getApiResources: build.query({ + query: () => ({ url: `/` }), + providesTags: ['API Discovery'], + }), listCheck: build.query({ query: (queryArg) => ({ url: `/checks`, @@ -39,6 +43,29 @@ const injectedRtkApi = api }), invalidatesTags: ['Check'], }), + deletecollectionCheck: build.mutation({ + query: (queryArg) => ({ + url: `/checks`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['Check'], + }), getCheck: build.query({ query: (queryArg) => ({ url: `/checks/${queryArg.name}`, @@ -48,6 +75,20 @@ const injectedRtkApi = api }), providesTags: ['Check'], }), + replaceCheck: build.mutation({ + query: (queryArg) => ({ + url: `/checks/${queryArg.name}`, + method: 'PUT', + body: queryArg.check, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Check'], + }), deleteCheck: build.mutation({ query: (queryArg) => ({ url: `/checks/${queryArg.name}`, @@ -78,6 +119,44 @@ const injectedRtkApi = api }), invalidatesTags: ['Check'], }), + getCheckStatus: build.query({ + query: (queryArg) => ({ + url: `/checks/${queryArg.name}/status`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Check'], + }), + replaceCheckStatus: build.mutation({ + query: (queryArg) => ({ + url: `/checks/${queryArg.name}/status`, + method: 'PUT', + body: queryArg.check, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Check'], + }), + updateCheckStatus: build.mutation({ + query: (queryArg) => ({ + url: `/checks/${queryArg.name}/status`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Check'], + }), listCheckType: build.query({ query: (queryArg) => ({ url: `/checktypes`, @@ -97,6 +176,81 @@ const injectedRtkApi = api }), providesTags: ['CheckType'], }), + createCheckType: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes`, + method: 'POST', + body: queryArg.checkType, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['CheckType'], + }), + deletecollectionCheckType: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['CheckType'], + }), + getCheckType: build.query({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['CheckType'], + }), + replaceCheckType: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}`, + method: 'PUT', + body: queryArg.checkType, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['CheckType'], + }), + deleteCheckType: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['CheckType'], + }), updateCheckType: build.mutation({ query: (queryArg) => ({ url: `/checktypes/${queryArg.name}`, @@ -112,10 +266,50 @@ const injectedRtkApi = api }), invalidatesTags: ['CheckType'], }), + getCheckTypeStatus: build.query({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}/status`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['CheckType'], + }), + replaceCheckTypeStatus: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}/status`, + method: 'PUT', + body: queryArg.checkType, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['CheckType'], + }), + updateCheckTypeStatus: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}/status`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['CheckType'], + }), }), overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; +export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; +export type GetApiResourcesApiArg = void; export type ListCheckApiResponse = /** status 200 OK */ CheckList; export type ListCheckApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -176,6 +370,57 @@ export type CreateCheckApiArg = { fieldValidation?: string; check: Check; }; +export type DeletecollectionCheckApiResponse = /** status 200 OK */ Status; +export type DeletecollectionCheckApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; export type GetCheckApiResponse = /** status 200 OK */ Check; export type GetCheckApiArg = { /** name of the Check */ @@ -183,6 +428,20 @@ export type GetCheckApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; }; +export type ReplaceCheckApiResponse = /** status 200 OK */ Check | /** status 201 Created */ Check; +export type ReplaceCheckApiArg = { + /** name of the Check */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + check: Check; +}; export type DeleteCheckApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; export type DeleteCheckApiArg = { /** name of the Check */ @@ -216,6 +475,43 @@ export type UpdateCheckApiArg = { force?: boolean; patch: Patch; }; +export type GetCheckStatusApiResponse = /** status 200 OK */ Check; +export type GetCheckStatusApiArg = { + /** name of the Check */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceCheckStatusApiResponse = /** status 200 OK */ Check | /** status 201 Created */ Check; +export type ReplaceCheckStatusApiArg = { + /** name of the Check */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + check: Check; +}; +export type UpdateCheckStatusApiResponse = /** status 200 OK */ Check | /** status 201 Created */ Check; +export type UpdateCheckStatusApiArg = { + /** name of the Check */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; export type ListCheckTypeApiResponse = /** status 200 OK */ CheckTypeList; export type ListCheckTypeApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -261,6 +557,110 @@ export type ListCheckTypeApiArg = { /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ watch?: boolean; }; +export type CreateCheckTypeApiResponse = /** status 200 OK */ + | CheckType + | /** status 201 Created */ CheckType + | /** status 202 Accepted */ CheckType; +export type CreateCheckTypeApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + checkType: CheckType; +}; +export type DeletecollectionCheckTypeApiResponse = /** status 200 OK */ Status; +export type DeletecollectionCheckTypeApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetCheckTypeApiResponse = /** status 200 OK */ CheckType; +export type GetCheckTypeApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceCheckTypeApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; +export type ReplaceCheckTypeApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + checkType: CheckType; +}; +export type DeleteCheckTypeApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteCheckTypeApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; export type UpdateCheckTypeApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; export type UpdateCheckTypeApiArg = { /** name of the CheckType */ @@ -277,6 +677,75 @@ export type UpdateCheckTypeApiArg = { force?: boolean; patch: Patch; }; +export type GetCheckTypeStatusApiResponse = /** status 200 OK */ CheckType; +export type GetCheckTypeStatusApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceCheckTypeStatusApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; +export type ReplaceCheckTypeStatusApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + checkType: CheckType; +}; +export type UpdateCheckTypeStatusApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; +export type UpdateCheckTypeStatusApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type ApiResource = { + /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ + categories?: string[]; + /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ + group?: string; + /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ + kind: string; + /** name is the plural name of the resource. */ + name: string; + /** namespaced indicates if a resource is namespaced or not. */ + namespaced: boolean; + /** shortNames is a list of suggested short names of the resource. */ + shortNames?: string[]; + /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ + singularName: string; + /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ + storageVersionHash?: string; + /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ + verbs: string[]; + /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ + version?: string; +}; +export type ApiResourceList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** groupVersion is the group and version this APIResourceList is for. */ + groupVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** resources contains the name of the resources and if they are namespaced. */ + resources: ApiResource[]; +}; export type Time = string; export type FieldsV1 = object; export type ManagedFieldsEntry = { @@ -537,3 +1006,33 @@ export type CheckTypeList = { kind?: string; metadata: ListMeta; }; +export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, + useListCheckQuery, + useLazyListCheckQuery, + useCreateCheckMutation, + useDeletecollectionCheckMutation, + useGetCheckQuery, + useLazyGetCheckQuery, + useReplaceCheckMutation, + useDeleteCheckMutation, + useUpdateCheckMutation, + useGetCheckStatusQuery, + useLazyGetCheckStatusQuery, + useReplaceCheckStatusMutation, + useUpdateCheckStatusMutation, + useListCheckTypeQuery, + useLazyListCheckTypeQuery, + useCreateCheckTypeMutation, + useDeletecollectionCheckTypeMutation, + useGetCheckTypeQuery, + useLazyGetCheckTypeQuery, + useReplaceCheckTypeMutation, + useDeleteCheckTypeMutation, + useUpdateCheckTypeMutation, + useGetCheckTypeStatusQuery, + useLazyGetCheckTypeStatusQuery, + useReplaceCheckTypeStatusMutation, + useUpdateCheckTypeStatusMutation, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts index c5351dbfafb..15acb80900e 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts @@ -7,7 +7,7 @@ const injectedRtkApi = api .injectEndpoints({ endpoints: (build) => ({ getApiResources: build.query({ - query: () => ({ url: `/apis/correlations.grafana.app/v0alpha1/` }), + query: () => ({ url: `/` }), providesTags: ['API Discovery'], }), listCorrelation: build.query({ @@ -515,3 +515,16 @@ export type Status = { status?: string; }; export type Patch = object; +export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, + useListCorrelationQuery, + useLazyListCorrelationQuery, + useCreateCorrelationMutation, + useDeletecollectionCorrelationMutation, + useGetCorrelationQuery, + useLazyGetCorrelationQuery, + useReplaceCorrelationMutation, + useDeleteCorrelationMutation, + useUpdateCorrelationMutation, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts index 5abfbe269e8..26f8053d29c 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts @@ -1,11 +1,240 @@ import { api } from './baseAPI'; -export const addTagTypes = ['Search'] as const; +export const addTagTypes = ['API Discovery', 'Dashboard', 'LibraryPanel', 'Search'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ + getApiResources: build.query({ + query: () => ({ url: `/` }), + providesTags: ['API Discovery'], + }), + listDashboard: build.query({ + query: (queryArg) => ({ + url: `/dashboards`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['Dashboard'], + }), + createDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/dashboards`, + method: 'POST', + body: queryArg.dashboard, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Dashboard'], + }), + deletecollectionDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/dashboards`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['Dashboard'], + }), + getDashboard: build.query({ + query: (queryArg) => ({ + url: `/dashboards/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Dashboard'], + }), + replaceDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/dashboards/${queryArg.name}`, + method: 'PUT', + body: queryArg.dashboard, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Dashboard'], + }), + deleteDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/dashboards/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['Dashboard'], + }), + updateDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/dashboards/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Dashboard'], + }), + getDashboardDto: build.query({ + query: (queryArg) => ({ url: `/dashboards/${queryArg.name}/dto` }), + providesTags: ['Dashboard'], + }), + listLibraryPanel: build.query({ + query: (queryArg) => ({ + url: `/librarypanels`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['LibraryPanel'], + }), + createLibraryPanel: build.mutation({ + query: (queryArg) => ({ + url: `/librarypanels`, + method: 'POST', + body: queryArg.libraryPanel, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['LibraryPanel'], + }), + deletecollectionLibraryPanel: build.mutation< + DeletecollectionLibraryPanelApiResponse, + DeletecollectionLibraryPanelApiArg + >({ + query: (queryArg) => ({ + url: `/librarypanels`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['LibraryPanel'], + }), + getLibraryPanel: build.query({ + query: (queryArg) => ({ + url: `/librarypanels/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['LibraryPanel'], + }), + replaceLibraryPanel: build.mutation({ + query: (queryArg) => ({ + url: `/librarypanels/${queryArg.name}`, + method: 'PUT', + body: queryArg.libraryPanel, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['LibraryPanel'], + }), + deleteLibraryPanel: build.mutation({ + query: (queryArg) => ({ + url: `/librarypanels/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['LibraryPanel'], + }), + updateLibraryPanel: build.mutation({ + query: (queryArg) => ({ + url: `/librarypanels/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['LibraryPanel'], + }), getSearch: build.query({ query: (queryArg) => ({ url: `/search`, @@ -15,6 +244,7 @@ const injectedRtkApi = api folder: queryArg.folder, facet: queryArg.facet, tags: queryArg.tags, + libraryPanel: queryArg.libraryPanel, sort: queryArg.sort, limit: queryArg.limit, explain: queryArg.explain, @@ -22,10 +252,351 @@ const injectedRtkApi = api }), providesTags: ['Search'], }), + getSearchSortable: build.query({ + query: () => ({ url: `/search/sortable` }), + providesTags: ['Search'], + }), }), overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; +export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; +export type GetApiResourcesApiArg = void; +export type ListDashboardApiResponse = /** status 200 OK */ DashboardList; +export type ListDashboardApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateDashboardApiResponse = /** status 200 OK */ + | Dashboard + | /** status 201 Created */ Dashboard + | /** status 202 Accepted */ Dashboard; +export type CreateDashboardApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + dashboard: Dashboard; +}; +export type DeletecollectionDashboardApiResponse = /** status 200 OK */ Status; +export type DeletecollectionDashboardApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetDashboardApiResponse = /** status 200 OK */ Dashboard; +export type GetDashboardApiArg = { + /** name of the Dashboard */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceDashboardApiResponse = /** status 200 OK */ Dashboard | /** status 201 Created */ Dashboard; +export type ReplaceDashboardApiArg = { + /** name of the Dashboard */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + dashboard: Dashboard; +}; +export type DeleteDashboardApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteDashboardApiArg = { + /** name of the Dashboard */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateDashboardApiResponse = /** status 200 OK */ Dashboard | /** status 201 Created */ Dashboard; +export type UpdateDashboardApiArg = { + /** name of the Dashboard */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type GetDashboardDtoApiResponse = /** status 200 OK */ DashboardWithAccessInfo; +export type GetDashboardDtoApiArg = { + /** name of the DashboardWithAccessInfo */ + name: string; +}; +export type ListLibraryPanelApiResponse = /** status 200 OK */ LibraryPanelList; +export type ListLibraryPanelApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateLibraryPanelApiResponse = /** status 200 OK */ + | LibraryPanel + | /** status 201 Created */ LibraryPanel + | /** status 202 Accepted */ LibraryPanel; +export type CreateLibraryPanelApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + libraryPanel: LibraryPanel; +}; +export type DeletecollectionLibraryPanelApiResponse = /** status 200 OK */ Status; +export type DeletecollectionLibraryPanelApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetLibraryPanelApiResponse = /** status 200 OK */ LibraryPanel; +export type GetLibraryPanelApiArg = { + /** name of the LibraryPanel */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceLibraryPanelApiResponse = /** status 200 OK */ LibraryPanel | /** status 201 Created */ LibraryPanel; +export type ReplaceLibraryPanelApiArg = { + /** name of the LibraryPanel */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + libraryPanel: LibraryPanel; +}; +export type DeleteLibraryPanelApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteLibraryPanelApiArg = { + /** name of the LibraryPanel */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateLibraryPanelApiResponse = /** status 200 OK */ LibraryPanel | /** status 201 Created */ LibraryPanel; +export type UpdateLibraryPanelApiArg = { + /** name of the LibraryPanel */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; export type GetSearchApiResponse = /** status 200 undefined */ SearchResults; export type GetSearchApiArg = { /** user query string */ @@ -38,6 +609,8 @@ export type GetSearchApiArg = { facet?: string[]; /** tag query filter */ tags?: string[]; + /** find dashboards that reference a given libraryPanel */ + libraryPanel?: string; /** sortable field */ sort?: string; /** number of results to return */ @@ -45,6 +618,391 @@ export type GetSearchApiArg = { /** add debugging info that may help explain why the result matched */ explain?: boolean; }; +export type GetSearchSortableApiResponse = /** status 200 undefined */ { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Sortable fields (depends on backend support) */ + fields: any[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; +}; +export type GetSearchSortableApiArg = void; +export type ApiResource = { + /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ + categories?: string[]; + /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ + group?: string; + /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ + kind: string; + /** name is the plural name of the resource. */ + name: string; + /** namespaced indicates if a resource is namespaced or not. */ + namespaced: boolean; + /** shortNames is a list of suggested short names of the resource. */ + shortNames?: string[]; + /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ + singularName: string; + /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ + storageVersionHash?: string; + /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ + verbs: string[]; + /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ + version?: string; +}; +export type ApiResourceList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** groupVersion is the group and version this APIResourceList is for. */ + groupVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** resources contains the name of the resources and if they are namespaced. */ + resources: ApiResource[]; +}; +export type Time = string; +export type FieldsV1 = object; +export type ManagedFieldsEntry = { + /** APIVersion defines the version of this resource that this field set applies to. The format is "group/version" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted. */ + apiVersion?: string; + /** FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: "FieldsV1" */ + fieldsType?: string; + /** FieldsV1 holds the first JSON version format as described in the "FieldsV1" type. */ + fieldsV1?: FieldsV1; + /** Manager is an identifier of the workflow managing these fields. */ + manager?: string; + /** Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'. */ + operation?: string; + /** Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource. */ + subresource?: string; + /** Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over. */ + time?: Time; +}; +export type OwnerReference = { + /** API version of the referent. */ + apiVersion: string; + /** If true, AND if the owner has the "foregroundDeletion" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs "delete" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned. */ + blockOwnerDeletion?: boolean; + /** If true, this reference points to the managing controller. */ + controller?: boolean; + /** Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind: string; + /** Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ + name: string; + /** UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid: string; +}; +export type ObjectMeta = { + /** Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations */ + annotations?: { + [key: string]: string; + }; + /** CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + creationTimestamp?: Time; + /** Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. */ + deletionGracePeriodSeconds?: number; + /** DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested. + + Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + deletionTimestamp?: Time; + /** Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list. */ + finalizers?: string[]; + /** GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server. + + If this field is specified and the generated name exists, the server will return a 409. + + Applied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency */ + generateName?: string; + /** A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. */ + generation?: number; + /** Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels */ + labels?: { + [key: string]: string; + }; + /** ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like "ci-cd". The set of fields is always in the version that the workflow used when modifying the object. */ + managedFields?: ManagedFieldsEntry[]; + /** Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ + name?: string; + /** Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. + + Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces */ + namespace?: string; + /** List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. */ + ownerReferences?: OwnerReference[]; + /** An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. + + Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ + resourceVersion?: string; + /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ + selfLink?: string; + /** UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations. + + Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid?: string; +}; +export type Unstructured = { + [key: string]: any; +}; +export type DashboardConversionStatus = { + /** The error message from the conversion. Empty if the conversion has not failed. */ + error?: string; + /** Whether from another version has failed. If true, means that the dashboard is not valid, and the caller should instead fetch the stored version. */ + failed: boolean; + /** The original value map[string]any */ + source?: object; + /** The version which was stored when the dashboard was created / updated. Fetching this version should always succeed. */ + storedVersion?: string; +}; +export type DashboardStatus = { + /** Optional conversion status. */ + conversion?: DashboardConversionStatus; +}; +export type Dashboard = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the Dashboard */ + spec: Unstructured; + status: DashboardStatus; +}; +export type ListMeta = { + /** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */ + continue?: string; + /** remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. */ + remainingItemCount?: number; + /** String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ + resourceVersion?: string; + /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ + selfLink?: string; +}; +export type DashboardList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: Dashboard[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type StatusCause = { + /** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. + + Examples: + "name" - the field "name" on the current resource + "items[0].name" - the field "name" on the first array entry in "items" */ + field?: string; + /** A human-readable description of the cause of the error. This field may be presented as-is to a reader. */ + message?: string; + /** A machine-readable description of the cause of the error. If this value is empty there is no information available. */ + reason?: string; +}; +export type StatusDetails = { + /** The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. */ + causes?: StatusCause[]; + /** The group attribute of the resource associated with the status StatusReason. */ + group?: string; + /** The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described). */ + name?: string; + /** If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action. */ + retryAfterSeconds?: number; + /** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid?: string; +}; +export type Status = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Suggested HTTP return code for this status, 0 if not set. */ + code?: number; + /** Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type. */ + details?: StatusDetails; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** A human-readable description of the status of this operation. */ + message?: string; + /** Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + metadata?: ListMeta; + /** A machine-readable description of why this operation is in the "Failure" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. */ + reason?: string; + /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */ + status?: string; +}; +export type Patch = object; +export type AnnotationActions = { + canAdd: boolean; + canDelete: boolean; + canEdit: boolean; +}; +export type AnnotationPermission = { + dashboard: AnnotationActions; + organization: AnnotationActions; +}; +export type DashboardAccess = { + annotationsPermissions: AnnotationPermission; + canAdmin: boolean; + canDelete: boolean; + canEdit: boolean; + /** The permissions part */ + canSave: boolean; + canStar: boolean; + isPublic: boolean; + /** Metadata fields */ + slug?: string; + url?: string; +}; +export type DashboardWithAccessInfo = { + access: DashboardAccess; + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the Dashboard */ + spec: Unstructured; + status: DashboardStatus; +}; +export type DataSourceRef = { + /** The apiserver version */ + apiVersion?: string; + /** The datasource plugin type */ + type: string; + /** Datasource UID (NOTE: name in k8s) */ + uid?: string; +}; +export type GridPos = { + h: number; + w: number; + x: number; + y: number; +}; +export type DataQuery = { + /** The datasource */ + datasource?: { + /** The apiserver version */ + apiVersion?: string; + /** The datasource plugin type */ + type: string; + /** Datasource UID (NOTE: name in k8s) */ + uid?: string; + }; + /** true if query is disabled (ie should not be returned to the dashboard) + NOTE: this does not always imply that the query should not be executed since + the results from a hidden query may be used as the input to other queries (SSE etc) */ + hide?: boolean; + /** Interval is the suggested duration between time points in a time series query. + NOTE: the values for intervalMs is not saved in the query model. It is typically calculated + from the interval required to fill a pixels in the visualization */ + intervalMs?: number; + /** MaxDataPoints is the maximum number of data points that should be returned from a time series query. + NOTE: the values for maxDataPoints is not saved in the query model. It is typically calculated + from the number of pixels visible in a visualization */ + maxDataPoints?: number; + /** QueryType is an optional identifier for the type of query. + It can be used to distinguish different types of queries. */ + queryType?: string; + /** RefID is the unique identifier of the query, set by the frontend call. */ + refId?: string; + /** Optionally define expected query result behavior */ + resultAssertions?: { + /** Maximum frame count */ + maxFrames?: number; + /** Type asserts that the frame matches a known type structure. + + + Possible enum values: + - `""` + - `"timeseries-wide"` + - `"timeseries-long"` + - `"timeseries-many"` + - `"timeseries-multi"` + - `"directory-listing"` + - `"table"` + - `"numeric-wide"` + - `"numeric-multi"` + - `"numeric-long"` + - `"log-lines"` */ + type?: + | '' + | 'timeseries-wide' + | 'timeseries-long' + | 'timeseries-many' + | 'timeseries-multi' + | 'directory-listing' + | 'table' + | 'numeric-wide' + | 'numeric-multi' + | 'numeric-long' + | 'log-lines'; + /** TypeVersion is the version of the Type property. Versions greater than 0.0 correspond to the dataplane + contract documentation https://grafana.github.io/dataplane/contract/. */ + typeVersion: number[]; + }; + /** TimeRange represents the query range + NOTE: unlike generic /ds/query, we can now send explicit time values in each query + NOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly */ + timeRange?: { + /** From is the start time of the query. */ + from: string; + /** To is the end time of the query. */ + to: string; + }; + [key: string]: any; +}; +export type LibraryPanelSpec = { + /** The default datasource type */ + datasource?: DataSourceRef; + /** Library panel description */ + description?: string; + /** The fieldConfig schema depends on the panel type */ + fieldConfig: Unstructured; + /** The grid position */ + gridPos?: GridPos; + /** The links for the panel */ + links?: Unstructured[]; + /** The options schema depends on the panel type */ + options: Unstructured; + /** The title of the panel when displayed in the dashboard */ + panelTitle?: string; + /** The panel type */ + pluginVersion?: string; + /** The datasource queries */ + targets?: DataQuery[]; + /** The title of the library panel */ + title?: string; + /** Whether the panel is transparent */ + transparent?: boolean; + /** The panel type */ + type: string; +}; +export type LibraryPanelStatus = { + /** The properties previously stored in SQL that are not included in this model */ + missing?: Unstructured; + /** Translation warnings (mostly things that were in SQL columns but not found in the saved body) */ + warnings?: string[]; +}; +export type LibraryPanel = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + metadata?: ObjectMeta; + /** Panel properties */ + spec: LibraryPanelSpec; + /** Status will show errors */ + status?: LibraryPanelStatus; +}; +export type LibraryPanelList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: LibraryPanel[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: ListMeta; +}; export type TermFacet = { count?: number; term?: string; @@ -105,3 +1063,31 @@ export type SearchResults = { /** The number of matching results */ totalHits: number; }; +export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, + useListDashboardQuery, + useLazyListDashboardQuery, + useCreateDashboardMutation, + useDeletecollectionDashboardMutation, + useGetDashboardQuery, + useLazyGetDashboardQuery, + useReplaceDashboardMutation, + useDeleteDashboardMutation, + useUpdateDashboardMutation, + useGetDashboardDtoQuery, + useLazyGetDashboardDtoQuery, + useListLibraryPanelQuery, + useLazyListLibraryPanelQuery, + useCreateLibraryPanelMutation, + useDeletecollectionLibraryPanelMutation, + useGetLibraryPanelQuery, + useLazyGetLibraryPanelQuery, + useReplaceLibraryPanelMutation, + useDeleteLibraryPanelMutation, + useUpdateLibraryPanelMutation, + useGetSearchQuery, + useLazyGetSearchQuery, + useGetSearchSortableQuery, + useLazyGetSearchSortableQuery, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/endpoints.gen.ts index ae8bba67c4b..984d95dcd68 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/folder/v1beta1/endpoints.gen.ts @@ -7,7 +7,7 @@ const injectedRtkApi = api .injectEndpoints({ endpoints: (build) => ({ getApiResources: build.query({ - query: () => ({ url: `/apis/folder.grafana.app/v1beta1/` }), + query: () => ({ url: `/` }), providesTags: ['API Discovery'], }), listFolder: build.query({ @@ -560,3 +560,24 @@ export type FolderInfoList = { kind?: string; metadata?: ListMeta; }; +export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, + useListFolderQuery, + useLazyListFolderQuery, + useCreateFolderMutation, + useDeletecollectionFolderMutation, + useGetFolderQuery, + useLazyGetFolderQuery, + useReplaceFolderMutation, + useDeleteFolderMutation, + useUpdateFolderMutation, + useGetFolderAccessQuery, + useLazyGetFolderAccessQuery, + useGetFolderChildrenQuery, + useLazyGetFolderChildrenQuery, + useGetFolderCountsQuery, + useLazyGetFolderCountsQuery, + useGetFolderParentsQuery, + useLazyGetFolderParentsQuery, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts index d59b1d4cd52..851680b4be4 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts @@ -1,11 +1,24 @@ import { api } from './baseAPI'; -export const addTagTypes = ['Display'] as const; +export const addTagTypes = [ + 'API Discovery', + 'Display', + 'ExternalGroupMapping', + 'ServiceAccount', + 'SSOSetting', + 'TeamBinding', + 'Team', + 'User', +] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ + getApiResources: build.query({ + query: () => ({ url: `/` }), + providesTags: ['API Discovery'], + }), getDisplayMapping: build.query({ query: (queryArg) => ({ url: `/display`, @@ -15,15 +28,1646 @@ const injectedRtkApi = api }), providesTags: ['Display'], }), + listExternalGroupMapping: build.query({ + query: (queryArg) => ({ + url: `/externalgroupmappings`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['ExternalGroupMapping'], + }), + createExternalGroupMapping: build.mutation< + CreateExternalGroupMappingApiResponse, + CreateExternalGroupMappingApiArg + >({ + query: (queryArg) => ({ + url: `/externalgroupmappings`, + method: 'POST', + body: queryArg.externalGroupMapping, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['ExternalGroupMapping'], + }), + deletecollectionExternalGroupMapping: build.mutation< + DeletecollectionExternalGroupMappingApiResponse, + DeletecollectionExternalGroupMappingApiArg + >({ + query: (queryArg) => ({ + url: `/externalgroupmappings`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['ExternalGroupMapping'], + }), + getExternalGroupMapping: build.query({ + query: (queryArg) => ({ + url: `/externalgroupmappings/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['ExternalGroupMapping'], + }), + replaceExternalGroupMapping: build.mutation< + ReplaceExternalGroupMappingApiResponse, + ReplaceExternalGroupMappingApiArg + >({ + query: (queryArg) => ({ + url: `/externalgroupmappings/${queryArg.name}`, + method: 'PUT', + body: queryArg.externalGroupMapping, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['ExternalGroupMapping'], + }), + deleteExternalGroupMapping: build.mutation< + DeleteExternalGroupMappingApiResponse, + DeleteExternalGroupMappingApiArg + >({ + query: (queryArg) => ({ + url: `/externalgroupmappings/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['ExternalGroupMapping'], + }), + updateExternalGroupMapping: build.mutation< + UpdateExternalGroupMappingApiResponse, + UpdateExternalGroupMappingApiArg + >({ + query: (queryArg) => ({ + url: `/externalgroupmappings/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['ExternalGroupMapping'], + }), + listServiceAccount: build.query({ + query: (queryArg) => ({ + url: `/serviceaccounts`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['ServiceAccount'], + }), + createServiceAccount: build.mutation({ + query: (queryArg) => ({ + url: `/serviceaccounts`, + method: 'POST', + body: queryArg.serviceAccount, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['ServiceAccount'], + }), + deletecollectionServiceAccount: build.mutation< + DeletecollectionServiceAccountApiResponse, + DeletecollectionServiceAccountApiArg + >({ + query: (queryArg) => ({ + url: `/serviceaccounts`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['ServiceAccount'], + }), + getServiceAccount: build.query({ + query: (queryArg) => ({ + url: `/serviceaccounts/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['ServiceAccount'], + }), + replaceServiceAccount: build.mutation({ + query: (queryArg) => ({ + url: `/serviceaccounts/${queryArg.name}`, + method: 'PUT', + body: queryArg.serviceAccount, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['ServiceAccount'], + }), + deleteServiceAccount: build.mutation({ + query: (queryArg) => ({ + url: `/serviceaccounts/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['ServiceAccount'], + }), + updateServiceAccount: build.mutation({ + query: (queryArg) => ({ + url: `/serviceaccounts/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['ServiceAccount'], + }), + getServiceAccountTokens: build.query({ + query: (queryArg) => ({ url: `/serviceaccounts/${queryArg.name}/tokens` }), + providesTags: ['ServiceAccount'], + }), + listSsoSetting: build.query({ + query: (queryArg) => ({ + url: `/ssosettings`, + params: { + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + pretty: queryArg.pretty, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['SSOSetting'], + }), + getSsoSetting: build.query({ + query: (queryArg) => ({ + url: `/ssosettings/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['SSOSetting'], + }), + replaceSsoSetting: build.mutation({ + query: (queryArg) => ({ + url: `/ssosettings/${queryArg.name}`, + method: 'PUT', + body: queryArg.ssoSetting, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['SSOSetting'], + }), + deleteSsoSetting: build.mutation({ + query: (queryArg) => ({ + url: `/ssosettings/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['SSOSetting'], + }), + updateSsoSetting: build.mutation({ + query: (queryArg) => ({ + url: `/ssosettings/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['SSOSetting'], + }), + listTeamBinding: build.query({ + query: (queryArg) => ({ + url: `/teambindings`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['TeamBinding'], + }), + createTeamBinding: build.mutation({ + query: (queryArg) => ({ + url: `/teambindings`, + method: 'POST', + body: queryArg.teamBinding, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['TeamBinding'], + }), + deletecollectionTeamBinding: build.mutation< + DeletecollectionTeamBindingApiResponse, + DeletecollectionTeamBindingApiArg + >({ + query: (queryArg) => ({ + url: `/teambindings`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['TeamBinding'], + }), + getTeamBinding: build.query({ + query: (queryArg) => ({ + url: `/teambindings/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['TeamBinding'], + }), + replaceTeamBinding: build.mutation({ + query: (queryArg) => ({ + url: `/teambindings/${queryArg.name}`, + method: 'PUT', + body: queryArg.teamBinding, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['TeamBinding'], + }), + deleteTeamBinding: build.mutation({ + query: (queryArg) => ({ + url: `/teambindings/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['TeamBinding'], + }), + updateTeamBinding: build.mutation({ + query: (queryArg) => ({ + url: `/teambindings/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['TeamBinding'], + }), + listTeam: build.query({ + query: (queryArg) => ({ + url: `/teams`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['Team'], + }), + createTeam: build.mutation({ + query: (queryArg) => ({ + url: `/teams`, + method: 'POST', + body: queryArg.team, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Team'], + }), + deletecollectionTeam: build.mutation({ + query: (queryArg) => ({ + url: `/teams`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['Team'], + }), + getTeam: build.query({ + query: (queryArg) => ({ + url: `/teams/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Team'], + }), + replaceTeam: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.name}`, + method: 'PUT', + body: queryArg.team, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Team'], + }), + deleteTeam: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['Team'], + }), + updateTeam: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Team'], + }), + getTeamMembers: build.query({ + query: (queryArg) => ({ url: `/teams/${queryArg.name}/members` }), + providesTags: ['Team'], + }), + listUser: build.query({ + query: (queryArg) => ({ + url: `/users`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['User'], + }), + createUser: build.mutation({ + query: (queryArg) => ({ + url: `/users`, + method: 'POST', + body: queryArg.user, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['User'], + }), + deletecollectionUser: build.mutation({ + query: (queryArg) => ({ + url: `/users`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['User'], + }), + getUser: build.query({ + query: (queryArg) => ({ + url: `/users/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['User'], + }), + replaceUser: build.mutation({ + query: (queryArg) => ({ + url: `/users/${queryArg.name}`, + method: 'PUT', + body: queryArg.user, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['User'], + }), + deleteUser: build.mutation({ + query: (queryArg) => ({ + url: `/users/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['User'], + }), + updateUser: build.mutation({ + query: (queryArg) => ({ + url: `/users/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['User'], + }), + getUserTeams: build.query({ + query: (queryArg) => ({ url: `/users/${queryArg.name}/teams` }), + providesTags: ['User'], + }), }), overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; +export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; +export type GetApiResourcesApiArg = void; export type GetDisplayMappingApiResponse = /** status 200 undefined */ DisplayList; export type GetDisplayMappingApiArg = { /** Display keys */ key: string[]; }; +export type ListExternalGroupMappingApiResponse = /** status 200 OK */ ExternalGroupMappingList; +export type ListExternalGroupMappingApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateExternalGroupMappingApiResponse = /** status 200 OK */ + | ExternalGroupMapping + | /** status 201 Created */ ExternalGroupMapping + | /** status 202 Accepted */ ExternalGroupMapping; +export type CreateExternalGroupMappingApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + externalGroupMapping: ExternalGroupMapping; +}; +export type DeletecollectionExternalGroupMappingApiResponse = /** status 200 OK */ Status; +export type DeletecollectionExternalGroupMappingApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetExternalGroupMappingApiResponse = /** status 200 OK */ ExternalGroupMapping; +export type GetExternalGroupMappingApiArg = { + /** name of the ExternalGroupMapping */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceExternalGroupMappingApiResponse = /** status 200 OK */ + | ExternalGroupMapping + | /** status 201 Created */ ExternalGroupMapping; +export type ReplaceExternalGroupMappingApiArg = { + /** name of the ExternalGroupMapping */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + externalGroupMapping: ExternalGroupMapping; +}; +export type DeleteExternalGroupMappingApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteExternalGroupMappingApiArg = { + /** name of the ExternalGroupMapping */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateExternalGroupMappingApiResponse = /** status 200 OK */ + | ExternalGroupMapping + | /** status 201 Created */ ExternalGroupMapping; +export type UpdateExternalGroupMappingApiArg = { + /** name of the ExternalGroupMapping */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type ListServiceAccountApiResponse = /** status 200 OK */ ServiceAccountList; +export type ListServiceAccountApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateServiceAccountApiResponse = /** status 200 OK */ + | ServiceAccount + | /** status 201 Created */ ServiceAccount + | /** status 202 Accepted */ ServiceAccount; +export type CreateServiceAccountApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + serviceAccount: ServiceAccount; +}; +export type DeletecollectionServiceAccountApiResponse = /** status 200 OK */ Status; +export type DeletecollectionServiceAccountApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetServiceAccountApiResponse = /** status 200 OK */ ServiceAccount; +export type GetServiceAccountApiArg = { + /** name of the ServiceAccount */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceServiceAccountApiResponse = /** status 200 OK */ + | ServiceAccount + | /** status 201 Created */ ServiceAccount; +export type ReplaceServiceAccountApiArg = { + /** name of the ServiceAccount */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + serviceAccount: ServiceAccount; +}; +export type DeleteServiceAccountApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteServiceAccountApiArg = { + /** name of the ServiceAccount */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateServiceAccountApiResponse = /** status 200 OK */ + | ServiceAccount + | /** status 201 Created */ ServiceAccount; +export type UpdateServiceAccountApiArg = { + /** name of the ServiceAccount */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type GetServiceAccountTokensApiResponse = /** status 200 OK */ ServiceAccountTokenList; +export type GetServiceAccountTokensApiArg = { + /** name of the ServiceAccountTokenList */ + name: string; +}; +export type ListSsoSettingApiResponse = /** status 200 OK */ SsoSettingList; +export type ListSsoSettingApiArg = { + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type GetSsoSettingApiResponse = /** status 200 OK */ SsoSetting; +export type GetSsoSettingApiArg = { + /** name of the SSOSetting */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceSsoSettingApiResponse = /** status 200 OK */ SsoSetting | /** status 201 Created */ SsoSetting; +export type ReplaceSsoSettingApiArg = { + /** name of the SSOSetting */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + ssoSetting: SsoSetting; +}; +export type DeleteSsoSettingApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteSsoSettingApiArg = { + /** name of the SSOSetting */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateSsoSettingApiResponse = /** status 200 OK */ SsoSetting | /** status 201 Created */ SsoSetting; +export type UpdateSsoSettingApiArg = { + /** name of the SSOSetting */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type ListTeamBindingApiResponse = /** status 200 OK */ TeamBindingList; +export type ListTeamBindingApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateTeamBindingApiResponse = /** status 200 OK */ + | TeamBinding + | /** status 201 Created */ TeamBinding + | /** status 202 Accepted */ TeamBinding; +export type CreateTeamBindingApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + teamBinding: TeamBinding; +}; +export type DeletecollectionTeamBindingApiResponse = /** status 200 OK */ Status; +export type DeletecollectionTeamBindingApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetTeamBindingApiResponse = /** status 200 OK */ TeamBinding; +export type GetTeamBindingApiArg = { + /** name of the TeamBinding */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceTeamBindingApiResponse = /** status 200 OK */ TeamBinding | /** status 201 Created */ TeamBinding; +export type ReplaceTeamBindingApiArg = { + /** name of the TeamBinding */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + teamBinding: TeamBinding; +}; +export type DeleteTeamBindingApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteTeamBindingApiArg = { + /** name of the TeamBinding */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateTeamBindingApiResponse = /** status 200 OK */ TeamBinding | /** status 201 Created */ TeamBinding; +export type UpdateTeamBindingApiArg = { + /** name of the TeamBinding */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type ListTeamApiResponse = /** status 200 OK */ TeamList; +export type ListTeamApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateTeamApiResponse = /** status 200 OK */ + | Team + | /** status 201 Created */ Team + | /** status 202 Accepted */ Team; +export type CreateTeamApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + team: Team; +}; +export type DeletecollectionTeamApiResponse = /** status 200 OK */ Status; +export type DeletecollectionTeamApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetTeamApiResponse = /** status 200 OK */ Team; +export type GetTeamApiArg = { + /** name of the Team */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceTeamApiResponse = /** status 200 OK */ Team | /** status 201 Created */ Team; +export type ReplaceTeamApiArg = { + /** name of the Team */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + team: Team; +}; +export type DeleteTeamApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteTeamApiArg = { + /** name of the Team */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateTeamApiResponse = /** status 200 OK */ Team | /** status 201 Created */ Team; +export type UpdateTeamApiArg = { + /** name of the Team */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type GetTeamMembersApiResponse = /** status 200 OK */ TeamMemberList; +export type GetTeamMembersApiArg = { + /** name of the TeamMemberList */ + name: string; +}; +export type ListUserApiResponse = /** status 200 OK */ UserList; +export type ListUserApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateUserApiResponse = /** status 200 OK */ + | User + | /** status 201 Created */ User + | /** status 202 Accepted */ User; +export type CreateUserApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + user: User; +}; +export type DeletecollectionUserApiResponse = /** status 200 OK */ Status; +export type DeletecollectionUserApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetUserApiResponse = /** status 200 OK */ User; +export type GetUserApiArg = { + /** name of the User */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceUserApiResponse = /** status 200 OK */ User | /** status 201 Created */ User; +export type ReplaceUserApiArg = { + /** name of the User */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + user: User; +}; +export type DeleteUserApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteUserApiArg = { + /** name of the User */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateUserApiResponse = /** status 200 OK */ User | /** status 201 Created */ User; +export type UpdateUserApiArg = { + /** name of the User */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type GetUserTeamsApiResponse = /** status 200 OK */ UserTeamList; +export type GetUserTeamsApiArg = { + /** name of the UserTeamList */ + name: string; +}; +export type ApiResource = { + /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ + categories?: string[]; + /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ + group?: string; + /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ + kind: string; + /** name is the plural name of the resource. */ + name: string; + /** namespaced indicates if a resource is namespaced or not. */ + namespaced: boolean; + /** shortNames is a list of suggested short names of the resource. */ + shortNames?: string[]; + /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ + singularName: string; + /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ + storageVersionHash?: string; + /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ + verbs: string[]; + /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ + version?: string; +}; +export type ApiResourceList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** groupVersion is the group and version this APIResourceList is for. */ + groupVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** resources contains the name of the resources and if they are namespaced. */ + resources: ApiResource[]; +}; export type IdentityRef = { /** Name is the unique identifier for identity, guaranteed to be a unique value for the type within a namespace. */ name: string; @@ -62,3 +1706,411 @@ export type DisplayList = { kind?: string; metadata?: ListMeta; }; +export type Time = string; +export type FieldsV1 = object; +export type ManagedFieldsEntry = { + /** APIVersion defines the version of this resource that this field set applies to. The format is "group/version" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted. */ + apiVersion?: string; + /** FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: "FieldsV1" */ + fieldsType?: string; + /** FieldsV1 holds the first JSON version format as described in the "FieldsV1" type. */ + fieldsV1?: FieldsV1; + /** Manager is an identifier of the workflow managing these fields. */ + manager?: string; + /** Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'. */ + operation?: string; + /** Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource. */ + subresource?: string; + /** Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over. */ + time?: Time; +}; +export type OwnerReference = { + /** API version of the referent. */ + apiVersion: string; + /** If true, AND if the owner has the "foregroundDeletion" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs "delete" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned. */ + blockOwnerDeletion?: boolean; + /** If true, this reference points to the managing controller. */ + controller?: boolean; + /** Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind: string; + /** Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ + name: string; + /** UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid: string; +}; +export type ObjectMeta = { + /** Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations */ + annotations?: { + [key: string]: string; + }; + /** CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + creationTimestamp?: Time; + /** Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. */ + deletionGracePeriodSeconds?: number; + /** DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested. + + Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + deletionTimestamp?: Time; + /** Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list. */ + finalizers?: string[]; + /** GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server. + + If this field is specified and the generated name exists, the server will return a 409. + + Applied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency */ + generateName?: string; + /** A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. */ + generation?: number; + /** Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels */ + labels?: { + [key: string]: string; + }; + /** ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like "ci-cd". The set of fields is always in the version that the workflow used when modifying the object. */ + managedFields?: ManagedFieldsEntry[]; + /** Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ + name?: string; + /** Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. + + Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces */ + namespace?: string; + /** List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. */ + ownerReferences?: OwnerReference[]; + /** An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. + + Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ + resourceVersion?: string; + /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ + selfLink?: string; + /** UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations. + + Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid?: string; +}; +export type ExternalGroupMappingTeamRef = { + /** Name is the unique identifier for a team. */ + name: string; +}; +export type ExternalGroupMappingSpec = { + externalGroupId: string; + teamRef: ExternalGroupMappingTeamRef; +}; +export type ExternalGroupMapping = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the ExternalGroupMapping */ + spec: ExternalGroupMappingSpec; +}; +export type ExternalGroupMappingList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: ExternalGroupMapping[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type StatusCause = { + /** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. + + Examples: + "name" - the field "name" on the current resource + "items[0].name" - the field "name" on the first array entry in "items" */ + field?: string; + /** A human-readable description of the cause of the error. This field may be presented as-is to a reader. */ + message?: string; + /** A machine-readable description of the cause of the error. If this value is empty there is no information available. */ + reason?: string; +}; +export type StatusDetails = { + /** The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. */ + causes?: StatusCause[]; + /** The group attribute of the resource associated with the status StatusReason. */ + group?: string; + /** The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described). */ + name?: string; + /** If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action. */ + retryAfterSeconds?: number; + /** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid?: string; +}; +export type Status = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Suggested HTTP return code for this status, 0 if not set. */ + code?: number; + /** Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type. */ + details?: StatusDetails; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** A human-readable description of the status of this operation. */ + message?: string; + /** Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + metadata?: ListMeta; + /** A machine-readable description of why this operation is in the "Failure" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. */ + reason?: string; + /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */ + status?: string; +}; +export type Patch = object; +export type ServiceAccountSpec = { + disabled: boolean; + plugin: string; + role: string; + title: string; +}; +export type ServiceAccount = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the ServiceAccount */ + spec: ServiceAccountSpec; +}; +export type ServiceAccountList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: ServiceAccount[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type ServiceAccountToken = { + created: Time; + expires?: Time; + lastUsed?: Time; + name?: string; + revoked?: boolean; +}; +export type ServiceAccountTokenList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: ServiceAccountToken[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: ListMeta; +}; +export type Unstructured = { + [key: string]: any; +}; +export type SsoSettingSpec = { + settings: Unstructured; + /** Possible enum values: + - `"db"` + - `"system"` system is from config file, env or argument */ + source: 'db' | 'system'; +}; +export type SsoSetting = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + metadata?: ObjectMeta; + spec?: SsoSettingSpec; +}; +export type SsoSettingList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: SsoSetting[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: ListMeta; +}; +export type TeamBindingspecSubject = { + /** uid of the identity */ + name: string; +}; +export type TeamBindingTeamRef = { + /** Name is the unique identifier for a team. */ + name: string; +}; +export type TeamBindingSpec = { + external: boolean; + /** permission of the identity in the team */ + permission: string; + subject: TeamBindingspecSubject; + teamRef: TeamBindingTeamRef; +}; +export type TeamBinding = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the TeamBinding */ + spec: TeamBindingSpec; +}; +export type TeamBindingList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: TeamBinding[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type TeamSpec = { + email: string; + externalUID: string; + provisioned: boolean; + title: string; +}; +export type Team = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the Team */ + spec: TeamSpec; +}; +export type TeamList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: Team[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type TeamMember = { + /** AvatarURL is the url where we can get the avatar for identity */ + avatarURL?: string; + /** Display name for identity. */ + displayName: string; + /** External is set if member ship was synced from external IDP. */ + external?: boolean; + identity: IdentityRef; + /** InternalID is the legacy numeric id for identity, Deprecated: use the identityRef where possible */ + internalId?: number; + /** Permission member has in team. + + Possible enum values: + - `"admin"` + - `"member"` */ + permission?: 'admin' | 'member'; +}; +export type TeamMemberList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: TeamMember[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: ListMeta; +}; +export type UserSpec = { + disabled: boolean; + email: string; + emailVerified: boolean; + grafanaAdmin: boolean; + login: string; + provisioned: boolean; + role: string; + title: string; +}; +export type User = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the User */ + spec: UserSpec; +}; +export type UserList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: User[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type TeamRef = { + /** Name is the unique identifier for a team. */ + name?: string; +}; +export type UserTeam = { + /** Possible enum values: + - `"admin"` + - `"member"` */ + permission?: 'admin' | 'member'; + teamRef?: TeamRef; + title?: string; +}; +export type UserTeamList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: UserTeam[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: ListMeta; +}; +export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, + useGetDisplayMappingQuery, + useLazyGetDisplayMappingQuery, + useListExternalGroupMappingQuery, + useLazyListExternalGroupMappingQuery, + useCreateExternalGroupMappingMutation, + useDeletecollectionExternalGroupMappingMutation, + useGetExternalGroupMappingQuery, + useLazyGetExternalGroupMappingQuery, + useReplaceExternalGroupMappingMutation, + useDeleteExternalGroupMappingMutation, + useUpdateExternalGroupMappingMutation, + useListServiceAccountQuery, + useLazyListServiceAccountQuery, + useCreateServiceAccountMutation, + useDeletecollectionServiceAccountMutation, + useGetServiceAccountQuery, + useLazyGetServiceAccountQuery, + useReplaceServiceAccountMutation, + useDeleteServiceAccountMutation, + useUpdateServiceAccountMutation, + useGetServiceAccountTokensQuery, + useLazyGetServiceAccountTokensQuery, + useListSsoSettingQuery, + useLazyListSsoSettingQuery, + useGetSsoSettingQuery, + useLazyGetSsoSettingQuery, + useReplaceSsoSettingMutation, + useDeleteSsoSettingMutation, + useUpdateSsoSettingMutation, + useListTeamBindingQuery, + useLazyListTeamBindingQuery, + useCreateTeamBindingMutation, + useDeletecollectionTeamBindingMutation, + useGetTeamBindingQuery, + useLazyGetTeamBindingQuery, + useReplaceTeamBindingMutation, + useDeleteTeamBindingMutation, + useUpdateTeamBindingMutation, + useListTeamQuery, + useLazyListTeamQuery, + useCreateTeamMutation, + useDeletecollectionTeamMutation, + useGetTeamQuery, + useLazyGetTeamQuery, + useReplaceTeamMutation, + useDeleteTeamMutation, + useUpdateTeamMutation, + useGetTeamMembersQuery, + useLazyGetTeamMembersQuery, + useListUserQuery, + useLazyListUserQuery, + useCreateUserMutation, + useDeletecollectionUserMutation, + useGetUserQuery, + useLazyGetUserQuery, + useReplaceUserMutation, + useDeleteUserMutation, + useUpdateUserMutation, + useGetUserTeamsQuery, + useLazyGetUserTeamsQuery, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/index.ts b/packages/grafana-api-clients/src/clients/rtkq/index.ts index 77c02e78170..4bdede94ed9 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/index.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/index.ts @@ -11,7 +11,7 @@ import { generatedAPI as playlistAPIv0alpha1 } from './playlist/v0alpha1'; import { generatedAPI as preferencesUserAPI } from './preferences/user'; import { generatedAPI as preferencesAPIv1alpha1 } from './preferences/v1alpha1'; import { generatedAPI as provisioningAPIv0alpha1 } from './provisioning/v0alpha1'; -import { generatedAPI as shortURLAPIv1alpha1 } from './shorturl/v1alpha1'; +import { generatedAPI as shortURLAPIv1beta1 } from './shorturl/v1beta1'; import { generatedAPI as legacyUserAPI } from './user'; // PLOP_INJECT_IMPORT @@ -26,7 +26,7 @@ export const allMiddleware = [ preferencesAPIv1alpha1.middleware, preferencesUserAPI.middleware, provisioningAPIv0alpha1.middleware, - shortURLAPIv1alpha1.middleware, + shortURLAPIv1beta1.middleware, correlationsAPIv0alpha1.middleware, legacyUserAPI.middleware, // PLOP_INJECT_MIDDLEWARE @@ -43,7 +43,7 @@ export const allReducers = { [preferencesAPIv1alpha1.reducerPath]: preferencesAPIv1alpha1.reducer, [preferencesUserAPI.reducerPath]: preferencesUserAPI.reducer, [provisioningAPIv0alpha1.reducerPath]: provisioningAPIv0alpha1.reducer, - [shortURLAPIv1alpha1.reducerPath]: shortURLAPIv1alpha1.reducer, + [shortURLAPIv1beta1.reducerPath]: shortURLAPIv1beta1.reducer, [correlationsAPIv0alpha1.reducerPath]: correlationsAPIv0alpha1.reducer, [legacyUserAPI.reducerPath]: legacyUserAPI.reducer, // PLOP_INJECT_REDUCER diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/baseAPI.ts new file mode 100644 index 00000000000..a9a3a0c84bb --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/baseAPI.ts @@ -0,0 +1,9 @@ +import { createApi } from '@reduxjs/toolkit/query/react'; + +import { createBaseQuery } from '../createBaseQuery'; + +export const api = createApi({ + reducerPath: 'legacyAPI', + baseQuery: createBaseQuery({ baseURL: '/api' }), + endpoints: () => ({}), +}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts new file mode 100644 index 00000000000..058aa2b057c --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -0,0 +1,6980 @@ +import { api } from './baseAPI'; +export const addTagTypes = [ + 'enterprise', + 'access_control', + 'ldap_debug', + 'admin_ldap', + 'access_control_provisioning', + 'admin_provisioning', + 'admin', + 'admin_users', + 'quota', + 'annotations', + 'devices', + 'migrations', + 'convert_prometheus', + 'dashboards', + 'snapshots', + 'dashboard_public', + 'permissions', + 'versions', + 'datasources', + 'correlations', + 'health', + 'folders', + 'group_attribute_sync', + 'library_elements', + 'licensing', + 'saml', + 'org', + 'invites', + 'preferences', + 'orgs', + 'playlists', + 'query_history', + 'recording_rules', + 'reports', + 'search', + 'service_accounts', + 'signing_keys', + 'teams', + 'sync_team_groups', + 'signed_in_user', + 'user', + 'users', + 'provisioning', + 'sso_settings', +] as const; +const injectedRtkApi = api + .enhanceEndpoints({ + addTagTypes, + }) + .injectEndpoints({ + endpoints: (build) => ({ + searchResult: build.mutation({ + query: () => ({ url: `/access-control/assignments/search`, method: 'POST' }), + invalidatesTags: ['enterprise'], + }), + listRoles: build.query({ + query: (queryArg) => ({ + url: `/access-control/roles`, + params: { + delegatable: queryArg.delegatable, + includeHidden: queryArg.includeHidden, + }, + }), + providesTags: ['access_control', 'enterprise'], + }), + createRole: build.mutation({ + query: (queryArg) => ({ url: `/access-control/roles`, method: 'POST', body: queryArg.createRoleForm }), + invalidatesTags: ['access_control', 'enterprise'], + }), + deleteRole: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/roles/${queryArg.roleUid}`, + method: 'DELETE', + params: { + force: queryArg.force, + global: queryArg['global'], + }, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + getRole: build.query({ + query: (queryArg) => ({ url: `/access-control/roles/${queryArg.roleUid}` }), + providesTags: ['access_control', 'enterprise'], + }), + updateRole: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/roles/${queryArg.roleUid}`, + method: 'PUT', + body: queryArg.updateRoleCommand, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + getRoleAssignments: build.query({ + query: (queryArg) => ({ url: `/access-control/roles/${queryArg.roleUid}/assignments` }), + providesTags: ['access_control', 'enterprise'], + }), + setRoleAssignments: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/roles/${queryArg.roleUid}/assignments`, + method: 'PUT', + body: queryArg.setRoleAssignmentsCommand, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + getAccessControlStatus: build.query({ + query: () => ({ url: `/access-control/status` }), + providesTags: ['access_control', 'enterprise'], + }), + listTeamsRoles: build.query({ + query: (queryArg) => ({ + url: `/access-control/teams/roles/search`, + method: 'POST', + body: queryArg.rolesSearchQuery, + }), + providesTags: ['access_control', 'enterprise'], + }), + listTeamRoles: build.query({ + query: (queryArg) => ({ url: `/access-control/teams/${queryArg.teamId}/roles` }), + providesTags: ['access_control', 'enterprise'], + }), + addTeamRole: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/teams/${queryArg.teamId}/roles`, + method: 'POST', + body: queryArg.addTeamRoleCommand, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + setTeamRoles: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/teams/${queryArg.teamId}/roles`, + method: 'PUT', + body: queryArg.setTeamRolesCommand, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + removeTeamRole: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/teams/${queryArg.teamId}/roles/${queryArg.roleUid}`, + method: 'DELETE', + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + listUsersRoles: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/users/roles/search`, + method: 'POST', + body: queryArg.rolesSearchQuery, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + listUserRoles: build.query({ + query: (queryArg) => ({ url: `/access-control/users/${queryArg.userId}/roles` }), + providesTags: ['access_control', 'enterprise'], + }), + addUserRole: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/users/${queryArg.userId}/roles`, + method: 'POST', + body: queryArg.addUserRoleCommand, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + setUserRoles: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/users/${queryArg.userId}/roles`, + method: 'PUT', + body: queryArg.setUserRolesCommand, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + removeUserRole: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/users/${queryArg.userId}/roles/${queryArg.roleUid}`, + method: 'DELETE', + params: { + global: queryArg['global'], + }, + }), + invalidatesTags: ['access_control', 'enterprise'], + }), + getResourceDescription: build.query({ + query: (queryArg) => ({ url: `/access-control/${queryArg.resource}/description` }), + providesTags: ['access_control'], + }), + getResourcePermissions: build.query({ + query: (queryArg) => ({ url: `/access-control/${queryArg.resource}/${queryArg.resourceId}` }), + providesTags: ['access_control'], + }), + setResourcePermissions: build.mutation({ + query: (queryArg) => ({ + url: `/access-control/${queryArg.resource}/${queryArg.resourceId}`, + method: 'POST', + body: queryArg.setPermissionsCommand, + }), + invalidatesTags: ['access_control'], + }), + setResourcePermissionsForBuiltInRole: build.mutation< + SetResourcePermissionsForBuiltInRoleApiResponse, + SetResourcePermissionsForBuiltInRoleApiArg + >({ + query: (queryArg) => ({ + url: `/access-control/${queryArg.resource}/${queryArg.resourceId}/builtInRoles/${queryArg.builtInRole}`, + method: 'POST', + body: queryArg.setPermissionCommand, + }), + invalidatesTags: ['access_control'], + }), + setResourcePermissionsForTeam: build.mutation< + SetResourcePermissionsForTeamApiResponse, + SetResourcePermissionsForTeamApiArg + >({ + query: (queryArg) => ({ + url: `/access-control/${queryArg.resource}/${queryArg.resourceId}/teams/${queryArg.teamId}`, + method: 'POST', + body: queryArg.setPermissionCommand, + }), + invalidatesTags: ['access_control'], + }), + setResourcePermissionsForUser: build.mutation< + SetResourcePermissionsForUserApiResponse, + SetResourcePermissionsForUserApiArg + >({ + query: (queryArg) => ({ + url: `/access-control/${queryArg.resource}/${queryArg.resourceId}/users/${queryArg.userId}`, + method: 'POST', + body: queryArg.setPermissionCommand, + }), + invalidatesTags: ['access_control'], + }), + getSyncStatus: build.query({ + query: () => ({ url: `/admin/ldap-sync-status` }), + providesTags: ['ldap_debug', 'enterprise'], + }), + reloadLdapCfg: build.mutation({ + query: () => ({ url: `/admin/ldap/reload`, method: 'POST' }), + invalidatesTags: ['admin_ldap'], + }), + getLdapStatus: build.query({ + query: () => ({ url: `/admin/ldap/status` }), + providesTags: ['admin_ldap'], + }), + postSyncUserWithLdap: build.mutation({ + query: (queryArg) => ({ url: `/admin/ldap/sync/${queryArg.userId}`, method: 'POST' }), + invalidatesTags: ['admin_ldap'], + }), + getUserFromLdap: build.query({ + query: (queryArg) => ({ url: `/admin/ldap/${queryArg.userName}` }), + providesTags: ['admin_ldap'], + }), + adminProvisioningReloadAccessControl: build.mutation< + AdminProvisioningReloadAccessControlApiResponse, + AdminProvisioningReloadAccessControlApiArg + >({ + query: () => ({ url: `/admin/provisioning/access-control/reload`, method: 'POST' }), + invalidatesTags: ['access_control_provisioning', 'enterprise'], + }), + adminProvisioningReloadDashboards: build.mutation< + AdminProvisioningReloadDashboardsApiResponse, + AdminProvisioningReloadDashboardsApiArg + >({ + query: () => ({ url: `/admin/provisioning/dashboards/reload`, method: 'POST' }), + invalidatesTags: ['admin_provisioning'], + }), + adminProvisioningReloadDatasources: build.mutation< + AdminProvisioningReloadDatasourcesApiResponse, + AdminProvisioningReloadDatasourcesApiArg + >({ + query: () => ({ url: `/admin/provisioning/datasources/reload`, method: 'POST' }), + invalidatesTags: ['admin_provisioning'], + }), + adminProvisioningReloadPlugins: build.mutation< + AdminProvisioningReloadPluginsApiResponse, + AdminProvisioningReloadPluginsApiArg + >({ + query: () => ({ url: `/admin/provisioning/plugins/reload`, method: 'POST' }), + invalidatesTags: ['admin_provisioning'], + }), + adminGetSettings: build.query({ + query: () => ({ url: `/admin/settings` }), + providesTags: ['admin'], + }), + adminGetStats: build.query({ + query: () => ({ url: `/admin/stats` }), + providesTags: ['admin'], + }), + adminCreateUser: build.mutation({ + query: (queryArg) => ({ url: `/admin/users`, method: 'POST', body: queryArg.adminCreateUserForm }), + invalidatesTags: ['admin_users'], + }), + adminDeleteUser: build.mutation({ + query: (queryArg) => ({ url: `/admin/users/${queryArg.userId}`, method: 'DELETE' }), + invalidatesTags: ['admin_users'], + }), + adminGetUserAuthTokens: build.query({ + query: (queryArg) => ({ url: `/admin/users/${queryArg.userId}/auth-tokens` }), + providesTags: ['admin_users'], + }), + adminDisableUser: build.mutation({ + query: (queryArg) => ({ url: `/admin/users/${queryArg.userId}/disable`, method: 'POST' }), + invalidatesTags: ['admin_users'], + }), + adminEnableUser: build.mutation({ + query: (queryArg) => ({ url: `/admin/users/${queryArg.userId}/enable`, method: 'POST' }), + invalidatesTags: ['admin_users'], + }), + adminLogoutUser: build.mutation({ + query: (queryArg) => ({ url: `/admin/users/${queryArg.userId}/logout`, method: 'POST' }), + invalidatesTags: ['admin_users'], + }), + adminUpdateUserPassword: build.mutation({ + query: (queryArg) => ({ + url: `/admin/users/${queryArg.userId}/password`, + method: 'PUT', + body: queryArg.adminUpdateUserPasswordForm, + }), + invalidatesTags: ['admin_users'], + }), + adminUpdateUserPermissions: build.mutation< + AdminUpdateUserPermissionsApiResponse, + AdminUpdateUserPermissionsApiArg + >({ + query: (queryArg) => ({ + url: `/admin/users/${queryArg.userId}/permissions`, + method: 'PUT', + body: queryArg.adminUpdateUserPermissionsForm, + }), + invalidatesTags: ['admin_users'], + }), + getUserQuota: build.query({ + query: (queryArg) => ({ url: `/admin/users/${queryArg.userId}/quotas` }), + providesTags: ['quota', 'admin_users'], + }), + updateUserQuota: build.mutation({ + query: (queryArg) => ({ + url: `/admin/users/${queryArg.userId}/quotas/${queryArg.quotaTarget}`, + method: 'PUT', + body: queryArg.updateQuotaCmd, + }), + invalidatesTags: ['quota', 'admin_users'], + }), + adminRevokeUserAuthToken: build.mutation({ + query: (queryArg) => ({ + url: `/admin/users/${queryArg.userId}/revoke-auth-token`, + method: 'POST', + body: queryArg.revokeAuthTokenCmd, + }), + invalidatesTags: ['admin_users'], + }), + getAnnotations: build.query({ + query: (queryArg) => ({ + url: `/annotations`, + params: { + from: queryArg['from'], + to: queryArg.to, + userId: queryArg.userId, + alertId: queryArg.alertId, + alertUID: queryArg.alertUid, + dashboardId: queryArg.dashboardId, + dashboardUID: queryArg.dashboardUid, + panelId: queryArg.panelId, + limit: queryArg.limit, + tags: queryArg.tags, + type: queryArg['type'], + matchAny: queryArg.matchAny, + }, + }), + providesTags: ['annotations'], + }), + postAnnotation: build.mutation({ + query: (queryArg) => ({ url: `/annotations`, method: 'POST', body: queryArg.postAnnotationsCmd }), + invalidatesTags: ['annotations'], + }), + postGraphiteAnnotation: build.mutation({ + query: (queryArg) => ({ + url: `/annotations/graphite`, + method: 'POST', + body: queryArg.postGraphiteAnnotationsCmd, + }), + invalidatesTags: ['annotations'], + }), + massDeleteAnnotations: build.mutation({ + query: (queryArg) => ({ + url: `/annotations/mass-delete`, + method: 'POST', + body: queryArg.massDeleteAnnotationsCmd, + }), + invalidatesTags: ['annotations'], + }), + getAnnotationTags: build.query({ + query: (queryArg) => ({ + url: `/annotations/tags`, + params: { + tag: queryArg.tag, + limit: queryArg.limit, + }, + }), + providesTags: ['annotations'], + }), + deleteAnnotationById: build.mutation({ + query: (queryArg) => ({ url: `/annotations/${queryArg.annotationId}`, method: 'DELETE' }), + invalidatesTags: ['annotations'], + }), + getAnnotationById: build.query({ + query: (queryArg) => ({ url: `/annotations/${queryArg.annotationId}` }), + providesTags: ['annotations'], + }), + patchAnnotation: build.mutation({ + query: (queryArg) => ({ + url: `/annotations/${queryArg.annotationId}`, + method: 'PATCH', + body: queryArg.patchAnnotationsCmd, + }), + invalidatesTags: ['annotations'], + }), + updateAnnotation: build.mutation({ + query: (queryArg) => ({ + url: `/annotations/${queryArg.annotationId}`, + method: 'PUT', + body: queryArg.updateAnnotationsCmd, + }), + invalidatesTags: ['annotations'], + }), + listDevices: build.query({ + query: () => ({ url: `/anonymous/devices` }), + providesTags: ['devices'], + }), + searchDevices: build.query({ + query: () => ({ url: `/anonymous/search` }), + providesTags: ['devices'], + }), + getSessionList: build.query({ + query: () => ({ url: `/cloudmigration/migration` }), + providesTags: ['migrations'], + }), + createSession: build.mutation({ + query: (queryArg) => ({ + url: `/cloudmigration/migration`, + method: 'POST', + body: queryArg.cloudMigrationSessionRequestDto, + }), + invalidatesTags: ['migrations'], + }), + deleteSession: build.mutation({ + query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}`, method: 'DELETE' }), + invalidatesTags: ['migrations'], + }), + getSession: build.query({ + query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}` }), + providesTags: ['migrations'], + }), + createSnapshot: build.mutation({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshot`, + method: 'POST', + body: queryArg.createSnapshotRequestDto, + }), + invalidatesTags: ['migrations'], + }), + getSnapshot: build.query({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}`, + params: { + resultPage: queryArg.resultPage, + resultLimit: queryArg.resultLimit, + resultSortColumn: queryArg.resultSortColumn, + resultSortOrder: queryArg.resultSortOrder, + errorsOnly: queryArg.errorsOnly, + }, + }), + providesTags: ['migrations'], + }), + cancelSnapshot: build.mutation({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}/cancel`, + method: 'POST', + }), + invalidatesTags: ['migrations'], + }), + uploadSnapshot: build.mutation({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshot/${queryArg.snapshotUid}/upload`, + method: 'POST', + }), + invalidatesTags: ['migrations'], + }), + getShapshotList: build.query({ + query: (queryArg) => ({ + url: `/cloudmigration/migration/${queryArg.uid}/snapshots`, + params: { + page: queryArg.page, + limit: queryArg.limit, + sort: queryArg.sort, + }, + }), + providesTags: ['migrations'], + }), + getResourceDependencies: build.query({ + query: () => ({ url: `/cloudmigration/resources/dependencies` }), + providesTags: ['migrations'], + }), + getCloudMigrationToken: build.query({ + query: () => ({ url: `/cloudmigration/token` }), + providesTags: ['migrations'], + }), + createCloudMigrationToken: build.mutation({ + query: () => ({ url: `/cloudmigration/token`, method: 'POST' }), + invalidatesTags: ['migrations'], + }), + deleteCloudMigrationToken: build.mutation({ + query: (queryArg) => ({ url: `/cloudmigration/token/${queryArg.uid}`, method: 'DELETE' }), + invalidatesTags: ['migrations'], + }), + routeConvertPrometheusCortexGetRules: build.query< + RouteConvertPrometheusCortexGetRulesApiResponse, + RouteConvertPrometheusCortexGetRulesApiArg + >({ + query: () => ({ url: `/convert/api/prom/rules` }), + providesTags: ['convert_prometheus'], + }), + routeConvertPrometheusCortexPostRuleGroups: build.mutation< + RouteConvertPrometheusCortexPostRuleGroupsApiResponse, + RouteConvertPrometheusCortexPostRuleGroupsApiArg + >({ + query: () => ({ url: `/convert/api/prom/rules`, method: 'POST' }), + invalidatesTags: ['convert_prometheus'], + }), + routeConvertPrometheusCortexDeleteNamespace: build.mutation< + RouteConvertPrometheusCortexDeleteNamespaceApiResponse, + RouteConvertPrometheusCortexDeleteNamespaceApiArg + >({ + query: (queryArg) => ({ url: `/convert/api/prom/rules/${queryArg.namespaceTitle}`, method: 'DELETE' }), + invalidatesTags: ['convert_prometheus'], + }), + routeConvertPrometheusCortexGetNamespace: build.query< + RouteConvertPrometheusCortexGetNamespaceApiResponse, + RouteConvertPrometheusCortexGetNamespaceApiArg + >({ + query: (queryArg) => ({ url: `/convert/api/prom/rules/${queryArg.namespaceTitle}` }), + providesTags: ['convert_prometheus'], + }), + routeConvertPrometheusCortexPostRuleGroup: build.mutation< + RouteConvertPrometheusCortexPostRuleGroupApiResponse, + RouteConvertPrometheusCortexPostRuleGroupApiArg + >({ + query: (queryArg) => ({ + url: `/convert/api/prom/rules/${queryArg.namespaceTitle}`, + method: 'POST', + body: queryArg.prometheusRuleGroup, + headers: { + 'x-grafana-alerting-datasource-uid': queryArg['x-grafana-alerting-datasource-uid'], + 'x-grafana-alerting-recording-rules-paused': queryArg['x-grafana-alerting-recording-rules-paused'], + 'x-grafana-alerting-alert-rules-paused': queryArg['x-grafana-alerting-alert-rules-paused'], + 'x-grafana-alerting-target-datasource-uid': queryArg['x-grafana-alerting-target-datasource-uid'], + 'x-grafana-alerting-folder-uid': queryArg['x-grafana-alerting-folder-uid'], + 'x-grafana-alerting-notification-receiver': queryArg['x-grafana-alerting-notification-receiver'], + }, + }), + invalidatesTags: ['convert_prometheus'], + }), + routeConvertPrometheusCortexDeleteRuleGroup: build.mutation< + RouteConvertPrometheusCortexDeleteRuleGroupApiResponse, + RouteConvertPrometheusCortexDeleteRuleGroupApiArg + >({ + query: (queryArg) => ({ + url: `/convert/api/prom/rules/${queryArg.namespaceTitle}/${queryArg.group}`, + method: 'DELETE', + }), + invalidatesTags: ['convert_prometheus'], + }), + routeConvertPrometheusCortexGetRuleGroup: build.query< + RouteConvertPrometheusCortexGetRuleGroupApiResponse, + RouteConvertPrometheusCortexGetRuleGroupApiArg + >({ + query: (queryArg) => ({ url: `/convert/api/prom/rules/${queryArg.namespaceTitle}/${queryArg.group}` }), + providesTags: ['convert_prometheus'], + }), + routeConvertPrometheusGetRules: build.query< + RouteConvertPrometheusGetRulesApiResponse, + RouteConvertPrometheusGetRulesApiArg + >({ + query: () => ({ url: `/convert/prometheus/config/v1/rules` }), + providesTags: ['convert_prometheus'], + }), + routeConvertPrometheusPostRuleGroups: build.mutation< + RouteConvertPrometheusPostRuleGroupsApiResponse, + RouteConvertPrometheusPostRuleGroupsApiArg + >({ + query: () => ({ url: `/convert/prometheus/config/v1/rules`, method: 'POST' }), + invalidatesTags: ['convert_prometheus'], + }), + routeConvertPrometheusDeleteNamespace: build.mutation< + RouteConvertPrometheusDeleteNamespaceApiResponse, + RouteConvertPrometheusDeleteNamespaceApiArg + >({ + query: (queryArg) => ({ + url: `/convert/prometheus/config/v1/rules/${queryArg.namespaceTitle}`, + method: 'DELETE', + }), + invalidatesTags: ['convert_prometheus'], + }), + routeConvertPrometheusGetNamespace: build.query< + RouteConvertPrometheusGetNamespaceApiResponse, + RouteConvertPrometheusGetNamespaceApiArg + >({ + query: (queryArg) => ({ url: `/convert/prometheus/config/v1/rules/${queryArg.namespaceTitle}` }), + providesTags: ['convert_prometheus'], + }), + routeConvertPrometheusPostRuleGroup: build.mutation< + RouteConvertPrometheusPostRuleGroupApiResponse, + RouteConvertPrometheusPostRuleGroupApiArg + >({ + query: (queryArg) => ({ + url: `/convert/prometheus/config/v1/rules/${queryArg.namespaceTitle}`, + method: 'POST', + body: queryArg.prometheusRuleGroup, + headers: { + 'x-grafana-alerting-datasource-uid': queryArg['x-grafana-alerting-datasource-uid'], + 'x-grafana-alerting-recording-rules-paused': queryArg['x-grafana-alerting-recording-rules-paused'], + 'x-grafana-alerting-alert-rules-paused': queryArg['x-grafana-alerting-alert-rules-paused'], + 'x-grafana-alerting-target-datasource-uid': queryArg['x-grafana-alerting-target-datasource-uid'], + 'x-grafana-alerting-folder-uid': queryArg['x-grafana-alerting-folder-uid'], + 'x-grafana-alerting-notification-receiver': queryArg['x-grafana-alerting-notification-receiver'], + }, + }), + invalidatesTags: ['convert_prometheus'], + }), + routeConvertPrometheusDeleteRuleGroup: build.mutation< + RouteConvertPrometheusDeleteRuleGroupApiResponse, + RouteConvertPrometheusDeleteRuleGroupApiArg + >({ + query: (queryArg) => ({ + url: `/convert/prometheus/config/v1/rules/${queryArg.namespaceTitle}/${queryArg.group}`, + method: 'DELETE', + }), + invalidatesTags: ['convert_prometheus'], + }), + routeConvertPrometheusGetRuleGroup: build.query< + RouteConvertPrometheusGetRuleGroupApiResponse, + RouteConvertPrometheusGetRuleGroupApiArg + >({ + query: (queryArg) => ({ + url: `/convert/prometheus/config/v1/rules/${queryArg.namespaceTitle}/${queryArg.group}`, + }), + providesTags: ['convert_prometheus'], + }), + searchDashboardSnapshots: build.query({ + query: (queryArg) => ({ + url: `/dashboard/snapshots`, + params: { + query: queryArg.query, + limit: queryArg.limit, + }, + }), + providesTags: ['dashboards', 'snapshots'], + }), + calculateDashboardDiff: build.mutation({ + query: (queryArg) => ({ url: `/dashboards/calculate-diff`, method: 'POST', body: queryArg.body }), + invalidatesTags: ['dashboards'], + }), + postDashboard: build.mutation({ + query: (queryArg) => ({ url: `/dashboards/db`, method: 'POST', body: queryArg.saveDashboardCommand }), + invalidatesTags: ['dashboards'], + }), + getHomeDashboard: build.query({ + query: () => ({ url: `/dashboards/home` }), + providesTags: ['dashboards'], + }), + importDashboard: build.mutation({ + query: (queryArg) => ({ url: `/dashboards/import`, method: 'POST', body: queryArg.importDashboardRequest }), + invalidatesTags: ['dashboards'], + }), + interpolateDashboard: build.mutation({ + query: () => ({ url: `/dashboards/interpolate`, method: 'POST' }), + invalidatesTags: ['dashboards'], + }), + listPublicDashboards: build.query({ + query: () => ({ url: `/dashboards/public-dashboards` }), + providesTags: ['dashboards', 'dashboard_public'], + }), + getDashboardTags: build.query({ + query: () => ({ url: `/dashboards/tags` }), + providesTags: ['dashboards'], + }), + getPublicDashboard: build.query({ + query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.dashboardUid}/public-dashboards` }), + providesTags: ['dashboards', 'dashboard_public'], + }), + createPublicDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/dashboards/uid/${queryArg.dashboardUid}/public-dashboards`, + method: 'POST', + body: queryArg.publicDashboardDto, + }), + invalidatesTags: ['dashboards', 'dashboard_public'], + }), + deletePublicDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/dashboards/uid/${queryArg.dashboardUid}/public-dashboards/${queryArg.uid}`, + method: 'DELETE', + }), + invalidatesTags: ['dashboards', 'dashboard_public'], + }), + updatePublicDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/dashboards/uid/${queryArg.dashboardUid}/public-dashboards/${queryArg.uid}`, + method: 'PATCH', + body: queryArg.publicDashboardDto, + }), + invalidatesTags: ['dashboards', 'dashboard_public'], + }), + deleteDashboardByUid: build.mutation({ + query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.uid}`, method: 'DELETE' }), + invalidatesTags: ['dashboards'], + }), + getDashboardByUid: build.query({ + query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.uid}` }), + providesTags: ['dashboards'], + }), + getDashboardPermissionsListByUid: build.query< + GetDashboardPermissionsListByUidApiResponse, + GetDashboardPermissionsListByUidApiArg + >({ + query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.uid}/permissions` }), + providesTags: ['dashboards', 'permissions'], + }), + updateDashboardPermissionsByUid: build.mutation< + UpdateDashboardPermissionsByUidApiResponse, + UpdateDashboardPermissionsByUidApiArg + >({ + query: (queryArg) => ({ + url: `/dashboards/uid/${queryArg.uid}/permissions`, + method: 'POST', + body: queryArg.updateDashboardAclCommand, + }), + invalidatesTags: ['dashboards', 'permissions'], + }), + restoreDashboardVersionByUid: build.mutation< + RestoreDashboardVersionByUidApiResponse, + RestoreDashboardVersionByUidApiArg + >({ + query: (queryArg) => ({ + url: `/dashboards/uid/${queryArg.uid}/restore`, + method: 'POST', + body: queryArg.restoreDashboardVersionCommand, + }), + invalidatesTags: ['dashboards', 'versions'], + }), + getDashboardVersionsByUid: build.query({ + query: (queryArg) => ({ + url: `/dashboards/uid/${queryArg.uid}/versions`, + params: { + limit: queryArg.limit, + start: queryArg.start, + }, + }), + providesTags: ['dashboards', 'versions'], + }), + getDashboardVersionByUid: build.query({ + query: (queryArg) => ({ url: `/dashboards/uid/${queryArg.uid}/versions/${queryArg.dashboardVersionId}` }), + providesTags: ['dashboards', 'versions'], + }), + getDataSources: build.query({ + query: () => ({ url: `/datasources` }), + providesTags: ['datasources'], + }), + addDataSource: build.mutation({ + query: (queryArg) => ({ url: `/datasources`, method: 'POST', body: queryArg.addDataSourceCommand }), + invalidatesTags: ['datasources'], + }), + getCorrelations: build.query({ + query: (queryArg) => ({ + url: `/datasources/correlations`, + params: { + limit: queryArg.limit, + page: queryArg.page, + sourceUID: queryArg.sourceUid, + }, + }), + providesTags: ['datasources', 'correlations'], + }), + getDataSourceIdByName: build.query({ + query: (queryArg) => ({ url: `/datasources/id/${queryArg.name}` }), + providesTags: ['datasources'], + }), + deleteDataSourceByName: build.mutation({ + query: (queryArg) => ({ url: `/datasources/name/${queryArg.name}`, method: 'DELETE' }), + invalidatesTags: ['datasources'], + }), + getDataSourceByName: build.query({ + query: (queryArg) => ({ url: `/datasources/name/${queryArg.name}` }), + providesTags: ['datasources'], + }), + datasourceProxyDeleteByUiDcalls: build.mutation< + DatasourceProxyDeleteByUiDcallsApiResponse, + DatasourceProxyDeleteByUiDcallsApiArg + >({ + query: (queryArg) => ({ + url: `/datasources/proxy/uid/${queryArg.uid}/${queryArg.datasourceProxyRoute}`, + method: 'DELETE', + }), + invalidatesTags: ['datasources'], + }), + datasourceProxyGetByUiDcalls: build.query< + DatasourceProxyGetByUiDcallsApiResponse, + DatasourceProxyGetByUiDcallsApiArg + >({ + query: (queryArg) => ({ url: `/datasources/proxy/uid/${queryArg.uid}/${queryArg.datasourceProxyRoute}` }), + providesTags: ['datasources'], + }), + datasourceProxyPostByUiDcalls: build.mutation< + DatasourceProxyPostByUiDcallsApiResponse, + DatasourceProxyPostByUiDcallsApiArg + >({ + query: (queryArg) => ({ + url: `/datasources/proxy/uid/${queryArg.uid}/${queryArg.datasourceProxyRoute}`, + method: 'POST', + body: queryArg.body, + }), + invalidatesTags: ['datasources'], + }), + getCorrelationsBySourceUid: build.query({ + query: (queryArg) => ({ url: `/datasources/uid/${queryArg.sourceUid}/correlations` }), + providesTags: ['datasources', 'correlations'], + }), + createCorrelation: build.mutation({ + query: (queryArg) => ({ + url: `/datasources/uid/${queryArg.sourceUid}/correlations`, + method: 'POST', + body: queryArg.createCorrelationCommand, + }), + invalidatesTags: ['datasources', 'correlations'], + }), + getCorrelation: build.query({ + query: (queryArg) => ({ + url: `/datasources/uid/${queryArg.sourceUid}/correlations/${queryArg.correlationUid}`, + }), + providesTags: ['datasources', 'correlations'], + }), + updateCorrelation: build.mutation({ + query: (queryArg) => ({ + url: `/datasources/uid/${queryArg.sourceUid}/correlations/${queryArg.correlationUid}`, + method: 'PATCH', + body: queryArg.updateCorrelationCommand, + }), + invalidatesTags: ['datasources', 'correlations'], + }), + deleteDataSourceByUid: build.mutation({ + query: (queryArg) => ({ url: `/datasources/uid/${queryArg.uid}`, method: 'DELETE' }), + invalidatesTags: ['datasources'], + }), + getDataSourceByUid: build.query({ + query: (queryArg) => ({ url: `/datasources/uid/${queryArg.uid}` }), + providesTags: ['datasources'], + }), + updateDataSourceByUid: build.mutation({ + query: (queryArg) => ({ + url: `/datasources/uid/${queryArg.uid}`, + method: 'PUT', + body: queryArg.updateDataSourceCommand, + }), + invalidatesTags: ['datasources'], + }), + deleteCorrelation: build.mutation({ + query: (queryArg) => ({ + url: `/datasources/uid/${queryArg.uid}/correlations/${queryArg.correlationUid}`, + method: 'DELETE', + }), + invalidatesTags: ['datasources', 'correlations'], + }), + checkDatasourceHealthWithUid: build.query< + CheckDatasourceHealthWithUidApiResponse, + CheckDatasourceHealthWithUidApiArg + >({ + query: (queryArg) => ({ url: `/datasources/uid/${queryArg.uid}/health` }), + providesTags: ['datasources', 'health'], + }), + getTeamLbacRulesApi: build.query({ + query: (queryArg) => ({ url: `/datasources/uid/${queryArg.uid}/lbac/teams` }), + providesTags: ['enterprise'], + }), + updateTeamLbacRulesApi: build.mutation({ + query: (queryArg) => ({ + url: `/datasources/uid/${queryArg.uid}/lbac/teams`, + method: 'PUT', + body: queryArg.updateTeamLbacCommand, + }), + invalidatesTags: ['enterprise'], + }), + callDatasourceResourceWithUid: build.query< + CallDatasourceResourceWithUidApiResponse, + CallDatasourceResourceWithUidApiArg + >({ + query: (queryArg) => ({ url: `/datasources/uid/${queryArg.uid}/resources/${queryArg.datasourceProxyRoute}` }), + providesTags: ['datasources'], + }), + getDataSourceCacheConfig: build.query({ + query: (queryArg) => ({ url: `/datasources/${queryArg.dataSourceUid}/cache` }), + providesTags: ['enterprise'], + }), + setDataSourceCacheConfig: build.mutation({ + query: (queryArg) => ({ + url: `/datasources/${queryArg.dataSourceUid}/cache`, + method: 'POST', + body: queryArg.cacheConfigSetter, + }), + invalidatesTags: ['enterprise'], + }), + cleanDataSourceCache: build.mutation({ + query: (queryArg) => ({ url: `/datasources/${queryArg.dataSourceUid}/cache/clean`, method: 'POST' }), + invalidatesTags: ['enterprise'], + }), + disableDataSourceCache: build.mutation({ + query: (queryArg) => ({ url: `/datasources/${queryArg.dataSourceUid}/cache/disable`, method: 'POST' }), + invalidatesTags: ['enterprise'], + }), + enableDataSourceCache: build.mutation({ + query: (queryArg) => ({ url: `/datasources/${queryArg.dataSourceUid}/cache/enable`, method: 'POST' }), + invalidatesTags: ['enterprise'], + }), + queryMetricsWithExpressions: build.mutation< + QueryMetricsWithExpressionsApiResponse, + QueryMetricsWithExpressionsApiArg + >({ + query: (queryArg) => ({ url: `/ds/query`, method: 'POST', body: queryArg.metricRequest }), + invalidatesTags: ['datasources'], + }), + getFolders: build.query({ + query: (queryArg) => ({ + url: `/folders`, + params: { + limit: queryArg.limit, + page: queryArg.page, + parentUid: queryArg.parentUid, + permission: queryArg.permission, + }, + }), + providesTags: ['folders'], + }), + createFolder: build.mutation({ + query: (queryArg) => ({ url: `/folders`, method: 'POST', body: queryArg.createFolderCommand }), + invalidatesTags: ['folders'], + }), + deleteFolder: build.mutation({ + query: (queryArg) => ({ + url: `/folders/${queryArg.folderUid}`, + method: 'DELETE', + params: { + forceDeleteRules: queryArg.forceDeleteRules, + }, + }), + invalidatesTags: ['folders'], + }), + getFolderByUid: build.query({ + query: (queryArg) => ({ url: `/folders/${queryArg.folderUid}` }), + providesTags: ['folders'], + }), + updateFolder: build.mutation({ + query: (queryArg) => ({ + url: `/folders/${queryArg.folderUid}`, + method: 'PUT', + body: queryArg.updateFolderCommand, + }), + invalidatesTags: ['folders'], + }), + getFolderDescendantCounts: build.query({ + query: (queryArg) => ({ url: `/folders/${queryArg.folderUid}/counts` }), + providesTags: ['folders'], + }), + moveFolder: build.mutation({ + query: (queryArg) => ({ + url: `/folders/${queryArg.folderUid}/move`, + method: 'POST', + body: queryArg.moveFolderCommand, + }), + invalidatesTags: ['folders'], + }), + getFolderPermissionList: build.query({ + query: (queryArg) => ({ url: `/folders/${queryArg.folderUid}/permissions` }), + providesTags: ['folders', 'permissions'], + }), + updateFolderPermissions: build.mutation({ + query: (queryArg) => ({ + url: `/folders/${queryArg.folderUid}/permissions`, + method: 'POST', + body: queryArg.updateDashboardAclCommand, + }), + invalidatesTags: ['folders', 'permissions'], + }), + getMappedGroups: build.query({ + query: () => ({ url: `/groupsync/groups` }), + providesTags: ['group_attribute_sync', 'enterprise'], + }), + deleteGroupMappings: build.mutation({ + query: (queryArg) => ({ url: `/groupsync/groups/${queryArg.groupId}`, method: 'DELETE' }), + invalidatesTags: ['group_attribute_sync', 'enterprise'], + }), + createGroupMappings: build.mutation({ + query: (queryArg) => ({ + url: `/groupsync/groups/${queryArg.groupId}`, + method: 'POST', + body: queryArg.groupAttributes, + }), + invalidatesTags: ['group_attribute_sync', 'enterprise'], + }), + updateGroupMappings: build.mutation({ + query: (queryArg) => ({ + url: `/groupsync/groups/${queryArg.groupId}`, + method: 'PUT', + body: queryArg.groupAttributes, + }), + invalidatesTags: ['group_attribute_sync', 'enterprise'], + }), + getGroupRoles: build.query({ + query: (queryArg) => ({ url: `/groupsync/groups/${queryArg.groupId}/roles` }), + providesTags: ['group_attribute_sync', 'enterprise'], + }), + getHealth: build.query({ + query: () => ({ url: `/health` }), + providesTags: ['health'], + }), + getLibraryElements: build.query({ + query: (queryArg) => ({ + url: `/library-elements`, + params: { + searchString: queryArg.searchString, + kind: queryArg.kind, + sortDirection: queryArg.sortDirection, + typeFilter: queryArg.typeFilter, + excludeUid: queryArg.excludeUid, + folderFilter: queryArg.folderFilter, + perPage: queryArg.perPage, + page: queryArg.page, + }, + }), + providesTags: ['library_elements'], + }), + createLibraryElement: build.mutation({ + query: (queryArg) => ({ url: `/library-elements`, method: 'POST', body: queryArg.createLibraryElementCommand }), + invalidatesTags: ['library_elements'], + }), + getLibraryElementByName: build.query({ + query: (queryArg) => ({ url: `/library-elements/name/${queryArg.libraryElementName}` }), + providesTags: ['library_elements'], + }), + deleteLibraryElementByUid: build.mutation({ + query: (queryArg) => ({ url: `/library-elements/${queryArg.libraryElementUid}`, method: 'DELETE' }), + invalidatesTags: ['library_elements'], + }), + getLibraryElementByUid: build.query({ + query: (queryArg) => ({ url: `/library-elements/${queryArg.libraryElementUid}` }), + providesTags: ['library_elements'], + }), + updateLibraryElement: build.mutation({ + query: (queryArg) => ({ + url: `/library-elements/${queryArg.libraryElementUid}`, + method: 'PATCH', + body: queryArg.patchLibraryElementCommand, + }), + invalidatesTags: ['library_elements'], + }), + getLibraryElementConnections: build.query< + GetLibraryElementConnectionsApiResponse, + GetLibraryElementConnectionsApiArg + >({ + query: (queryArg) => ({ url: `/library-elements/${queryArg.libraryElementUid}/connections/` }), + providesTags: ['library_elements'], + }), + getStatus: build.query({ + query: () => ({ url: `/licensing/check` }), + providesTags: ['licensing', 'enterprise'], + }), + refreshLicenseStats: build.query({ + query: () => ({ url: `/licensing/refresh-stats` }), + providesTags: ['licensing', 'enterprise'], + }), + deleteLicenseToken: build.mutation({ + query: (queryArg) => ({ url: `/licensing/token`, method: 'DELETE', body: queryArg.deleteTokenCommand }), + invalidatesTags: ['licensing', 'enterprise'], + }), + getLicenseToken: build.query({ + query: () => ({ url: `/licensing/token` }), + providesTags: ['licensing', 'enterprise'], + }), + postLicenseToken: build.mutation({ + query: (queryArg) => ({ url: `/licensing/token`, method: 'POST', body: queryArg.deleteTokenCommand }), + invalidatesTags: ['licensing', 'enterprise'], + }), + postRenewLicenseToken: build.mutation({ + query: (queryArg) => ({ url: `/licensing/token/renew`, method: 'POST', body: queryArg.body }), + invalidatesTags: ['licensing', 'enterprise'], + }), + getSamlLogout: build.query({ + query: () => ({ url: `/logout/saml` }), + providesTags: ['saml', 'enterprise'], + }), + getCurrentOrg: build.query({ + query: () => ({ url: `/org` }), + providesTags: ['org'], + }), + updateCurrentOrg: build.mutation({ + query: (queryArg) => ({ url: `/org`, method: 'PUT', body: queryArg.updateOrgForm }), + invalidatesTags: ['org'], + }), + updateCurrentOrgAddress: build.mutation({ + query: (queryArg) => ({ url: `/org/address`, method: 'PUT', body: queryArg.updateOrgAddressForm }), + invalidatesTags: ['org'], + }), + getPendingOrgInvites: build.query({ + query: () => ({ url: `/org/invites` }), + providesTags: ['org', 'invites'], + }), + addOrgInvite: build.mutation({ + query: (queryArg) => ({ url: `/org/invites`, method: 'POST', body: queryArg.addInviteForm }), + invalidatesTags: ['org', 'invites'], + }), + revokeInvite: build.mutation({ + query: (queryArg) => ({ url: `/org/invites/${queryArg.invitationCode}/revoke`, method: 'DELETE' }), + invalidatesTags: ['org', 'invites'], + }), + getOrgPreferences: build.query({ + query: () => ({ url: `/org/preferences` }), + providesTags: ['org', 'preferences'], + }), + patchOrgPreferences: build.mutation({ + query: (queryArg) => ({ url: `/org/preferences`, method: 'PATCH', body: queryArg.patchPrefsCmd }), + invalidatesTags: ['org', 'preferences'], + }), + updateOrgPreferences: build.mutation({ + query: (queryArg) => ({ url: `/org/preferences`, method: 'PUT', body: queryArg.updatePrefsCmd }), + invalidatesTags: ['org', 'preferences'], + }), + getCurrentOrgQuota: build.query({ + query: () => ({ url: `/org/quotas` }), + providesTags: ['quota', 'org'], + }), + getOrgUsersForCurrentOrg: build.query({ + query: (queryArg) => ({ + url: `/org/users`, + params: { + query: queryArg.query, + limit: queryArg.limit, + }, + }), + providesTags: ['org'], + }), + addOrgUserToCurrentOrg: build.mutation({ + query: (queryArg) => ({ url: `/org/users`, method: 'POST', body: queryArg.addOrgUserCommand }), + invalidatesTags: ['org'], + }), + getOrgUsersForCurrentOrgLookup: build.query< + GetOrgUsersForCurrentOrgLookupApiResponse, + GetOrgUsersForCurrentOrgLookupApiArg + >({ + query: (queryArg) => ({ + url: `/org/users/lookup`, + params: { + query: queryArg.query, + limit: queryArg.limit, + }, + }), + providesTags: ['org'], + }), + removeOrgUserForCurrentOrg: build.mutation< + RemoveOrgUserForCurrentOrgApiResponse, + RemoveOrgUserForCurrentOrgApiArg + >({ + query: (queryArg) => ({ url: `/org/users/${queryArg.userId}`, method: 'DELETE' }), + invalidatesTags: ['org'], + }), + updateOrgUserForCurrentOrg: build.mutation< + UpdateOrgUserForCurrentOrgApiResponse, + UpdateOrgUserForCurrentOrgApiArg + >({ + query: (queryArg) => ({ + url: `/org/users/${queryArg.userId}`, + method: 'PATCH', + body: queryArg.updateOrgUserCommand, + }), + invalidatesTags: ['org'], + }), + searchOrgs: build.query({ + query: (queryArg) => ({ + url: `/orgs`, + params: { + page: queryArg.page, + perpage: queryArg.perpage, + name: queryArg.name, + query: queryArg.query, + }, + }), + providesTags: ['orgs'], + }), + createOrg: build.mutation({ + query: (queryArg) => ({ url: `/orgs`, method: 'POST', body: queryArg.createOrgCommand }), + invalidatesTags: ['orgs'], + }), + getOrgByName: build.query({ + query: (queryArg) => ({ url: `/orgs/name/${queryArg.orgName}` }), + providesTags: ['orgs'], + }), + deleteOrgById: build.mutation({ + query: (queryArg) => ({ url: `/orgs/${queryArg.orgId}`, method: 'DELETE' }), + invalidatesTags: ['orgs'], + }), + getOrgById: build.query({ + query: (queryArg) => ({ url: `/orgs/${queryArg.orgId}` }), + providesTags: ['orgs'], + }), + updateOrg: build.mutation({ + query: (queryArg) => ({ url: `/orgs/${queryArg.orgId}`, method: 'PUT', body: queryArg.updateOrgForm }), + invalidatesTags: ['orgs'], + }), + updateOrgAddress: build.mutation({ + query: (queryArg) => ({ + url: `/orgs/${queryArg.orgId}/address`, + method: 'PUT', + body: queryArg.updateOrgAddressForm, + }), + invalidatesTags: ['orgs'], + }), + getOrgQuota: build.query({ + query: (queryArg) => ({ url: `/orgs/${queryArg.orgId}/quotas` }), + providesTags: ['quota', 'orgs'], + }), + updateOrgQuota: build.mutation({ + query: (queryArg) => ({ + url: `/orgs/${queryArg.orgId}/quotas/${queryArg.quotaTarget}`, + method: 'PUT', + body: queryArg.updateQuotaCmd, + }), + invalidatesTags: ['quota', 'orgs'], + }), + getOrgUsers: build.query({ + query: (queryArg) => ({ url: `/orgs/${queryArg.orgId}/users` }), + providesTags: ['orgs'], + }), + addOrgUser: build.mutation({ + query: (queryArg) => ({ + url: `/orgs/${queryArg.orgId}/users`, + method: 'POST', + body: queryArg.addOrgUserCommand, + }), + invalidatesTags: ['orgs'], + }), + searchOrgUsers: build.query({ + query: (queryArg) => ({ url: `/orgs/${queryArg.orgId}/users/search` }), + providesTags: ['orgs'], + }), + removeOrgUser: build.mutation({ + query: (queryArg) => ({ url: `/orgs/${queryArg.orgId}/users/${queryArg.userId}`, method: 'DELETE' }), + invalidatesTags: ['orgs'], + }), + updateOrgUser: build.mutation({ + query: (queryArg) => ({ + url: `/orgs/${queryArg.orgId}/users/${queryArg.userId}`, + method: 'PATCH', + body: queryArg.updateOrgUserCommand, + }), + invalidatesTags: ['orgs'], + }), + searchPlaylists: build.query({ + query: (queryArg) => ({ + url: `/playlists`, + params: { + query: queryArg.query, + limit: queryArg.limit, + }, + }), + providesTags: ['playlists'], + }), + createPlaylist: build.mutation({ + query: (queryArg) => ({ url: `/playlists`, method: 'POST', body: queryArg.createPlaylistCommand }), + invalidatesTags: ['playlists'], + }), + deletePlaylist: build.mutation({ + query: (queryArg) => ({ url: `/playlists/${queryArg.uid}`, method: 'DELETE' }), + invalidatesTags: ['playlists'], + }), + getPlaylist: build.query({ + query: (queryArg) => ({ url: `/playlists/${queryArg.uid}` }), + providesTags: ['playlists'], + }), + updatePlaylist: build.mutation({ + query: (queryArg) => ({ + url: `/playlists/${queryArg.uid}`, + method: 'PUT', + body: queryArg.updatePlaylistCommand, + }), + invalidatesTags: ['playlists'], + }), + getPlaylistItems: build.query({ + query: (queryArg) => ({ url: `/playlists/${queryArg.uid}/items` }), + providesTags: ['playlists'], + }), + viewPublicDashboard: build.query({ + query: (queryArg) => ({ url: `/public/dashboards/${queryArg.accessToken}` }), + providesTags: ['dashboards', 'dashboard_public'], + }), + getPublicAnnotations: build.query({ + query: (queryArg) => ({ url: `/public/dashboards/${queryArg.accessToken}/annotations` }), + providesTags: ['dashboards', 'annotations', 'dashboard_public'], + }), + queryPublicDashboard: build.mutation({ + query: (queryArg) => ({ + url: `/public/dashboards/${queryArg.accessToken}/panels/${queryArg.panelId}/query`, + method: 'POST', + }), + invalidatesTags: ['dashboards', 'dashboard_public'], + }), + searchQueries: build.query({ + query: (queryArg) => ({ + url: `/query-history`, + params: { + datasourceUid: queryArg.datasourceUid, + searchString: queryArg.searchString, + onlyStarred: queryArg.onlyStarred, + sort: queryArg.sort, + page: queryArg.page, + limit: queryArg.limit, + from: queryArg['from'], + to: queryArg.to, + }, + }), + providesTags: ['query_history'], + }), + createQuery: build.mutation({ + query: (queryArg) => ({ + url: `/query-history`, + method: 'POST', + body: queryArg.createQueryInQueryHistoryCommand, + }), + invalidatesTags: ['query_history'], + }), + unstarQuery: build.mutation({ + query: (queryArg) => ({ url: `/query-history/star/${queryArg.queryHistoryUid}`, method: 'DELETE' }), + invalidatesTags: ['query_history'], + }), + starQuery: build.mutation({ + query: (queryArg) => ({ url: `/query-history/star/${queryArg.queryHistoryUid}`, method: 'POST' }), + invalidatesTags: ['query_history'], + }), + deleteQuery: build.mutation({ + query: (queryArg) => ({ url: `/query-history/${queryArg.queryHistoryUid}`, method: 'DELETE' }), + invalidatesTags: ['query_history'], + }), + patchQueryComment: build.mutation({ + query: (queryArg) => ({ + url: `/query-history/${queryArg.queryHistoryUid}`, + method: 'PATCH', + body: queryArg.patchQueryCommentInQueryHistoryCommand, + }), + invalidatesTags: ['query_history'], + }), + listRecordingRules: build.query({ + query: () => ({ url: `/recording-rules` }), + providesTags: ['recording_rules', 'enterprise'], + }), + createRecordingRule: build.mutation({ + query: (queryArg) => ({ url: `/recording-rules`, method: 'POST', body: queryArg.recordingRuleJson }), + invalidatesTags: ['recording_rules', 'enterprise'], + }), + updateRecordingRule: build.mutation({ + query: (queryArg) => ({ url: `/recording-rules`, method: 'PUT', body: queryArg.recordingRuleJson }), + invalidatesTags: ['recording_rules', 'enterprise'], + }), + testCreateRecordingRule: build.mutation({ + query: (queryArg) => ({ url: `/recording-rules/test`, method: 'POST', body: queryArg.recordingRuleJson }), + invalidatesTags: ['recording_rules', 'enterprise'], + }), + deleteRecordingRuleWriteTarget: build.mutation< + DeleteRecordingRuleWriteTargetApiResponse, + DeleteRecordingRuleWriteTargetApiArg + >({ + query: () => ({ url: `/recording-rules/writer`, method: 'DELETE' }), + invalidatesTags: ['recording_rules', 'enterprise'], + }), + getRecordingRuleWriteTarget: build.query< + GetRecordingRuleWriteTargetApiResponse, + GetRecordingRuleWriteTargetApiArg + >({ + query: () => ({ url: `/recording-rules/writer` }), + providesTags: ['recording_rules', 'enterprise'], + }), + createRecordingRuleWriteTarget: build.mutation< + CreateRecordingRuleWriteTargetApiResponse, + CreateRecordingRuleWriteTargetApiArg + >({ + query: (queryArg) => ({ + url: `/recording-rules/writer`, + method: 'POST', + body: queryArg.prometheusRemoteWriteTargetJson, + }), + invalidatesTags: ['recording_rules', 'enterprise'], + }), + deleteRecordingRule: build.mutation({ + query: (queryArg) => ({ url: `/recording-rules/${queryArg.recordingRuleId}`, method: 'DELETE' }), + invalidatesTags: ['recording_rules', 'enterprise'], + }), + getReports: build.query({ + query: () => ({ url: `/reports` }), + providesTags: ['reports', 'enterprise'], + }), + createReport: build.mutation({ + query: (queryArg) => ({ url: `/reports`, method: 'POST', body: queryArg.createOrUpdateReport }), + invalidatesTags: ['reports', 'enterprise'], + }), + getReportsByDashboardUid: build.query({ + query: (queryArg) => ({ url: `/reports/dashboards/${queryArg.uid}` }), + providesTags: ['reports', 'enterprise'], + }), + sendReport: build.mutation({ + query: (queryArg) => ({ url: `/reports/email`, method: 'POST', body: queryArg.reportEmail }), + invalidatesTags: ['reports', 'enterprise'], + }), + getSettingsImage: build.query({ + query: () => ({ url: `/reports/images/:image` }), + providesTags: ['reports', 'enterprise'], + }), + renderReportCsVs: build.query({ + query: (queryArg) => ({ + url: `/reports/render/csvs`, + params: { + dashboards: queryArg.dashboards, + title: queryArg.title, + }, + }), + providesTags: ['reports', 'enterprise'], + }), + renderReportPdFs: build.query({ + query: (queryArg) => ({ + url: `/reports/render/pdfs`, + params: { + dashboards: queryArg.dashboards, + orientation: queryArg.orientation, + layout: queryArg.layout, + title: queryArg.title, + scaleFactor: queryArg.scaleFactor, + includeTables: queryArg.includeTables, + }, + }), + providesTags: ['reports', 'enterprise'], + }), + getReportSettings: build.query({ + query: () => ({ url: `/reports/settings` }), + providesTags: ['reports', 'enterprise'], + }), + saveReportSettings: build.mutation({ + query: (queryArg) => ({ url: `/reports/settings`, method: 'POST', body: queryArg.reportSettings }), + invalidatesTags: ['reports', 'enterprise'], + }), + sendTestEmail: build.mutation({ + query: (queryArg) => ({ url: `/reports/test-email`, method: 'POST', body: queryArg.createOrUpdateReport }), + invalidatesTags: ['reports', 'enterprise'], + }), + postAcs: build.mutation({ + query: (queryArg) => ({ + url: `/saml/acs`, + method: 'POST', + params: { + RelayState: queryArg.relayState, + }, + }), + invalidatesTags: ['saml', 'enterprise'], + }), + getMetadata: build.query({ + query: () => ({ url: `/saml/metadata` }), + providesTags: ['saml', 'enterprise'], + }), + getSlo: build.query({ + query: () => ({ url: `/saml/slo` }), + providesTags: ['saml', 'enterprise'], + }), + postSlo: build.mutation({ + query: (queryArg) => ({ + url: `/saml/slo`, + method: 'POST', + params: { + SAMLRequest: queryArg.samlRequest, + SAMLResponse: queryArg.samlResponse, + }, + }), + invalidatesTags: ['saml', 'enterprise'], + }), + search: build.query({ + query: (queryArg) => ({ + url: `/search`, + params: { + query: queryArg.query, + tag: queryArg.tag, + type: queryArg['type'], + dashboardIds: queryArg.dashboardIds, + dashboardUIDs: queryArg.dashboardUiDs, + folderIds: queryArg.folderIds, + folderUIDs: queryArg.folderUiDs, + starred: queryArg.starred, + limit: queryArg.limit, + page: queryArg.page, + permission: queryArg.permission, + sort: queryArg.sort, + deleted: queryArg.deleted, + }, + }), + providesTags: ['search'], + }), + listSortOptions: build.query({ + query: () => ({ url: `/search/sorting` }), + providesTags: ['search'], + }), + createServiceAccount: build.mutation({ + query: (queryArg) => ({ url: `/serviceaccounts`, method: 'POST', body: queryArg.createServiceAccountForm }), + invalidatesTags: ['service_accounts'], + }), + searchOrgServiceAccountsWithPaging: build.query< + SearchOrgServiceAccountsWithPagingApiResponse, + SearchOrgServiceAccountsWithPagingApiArg + >({ + query: (queryArg) => ({ + url: `/serviceaccounts/search`, + params: { + Disabled: queryArg.disabled, + expiredTokens: queryArg.expiredTokens, + query: queryArg.query, + perpage: queryArg.perpage, + page: queryArg.page, + }, + }), + providesTags: ['service_accounts'], + }), + deleteServiceAccount: build.mutation({ + query: (queryArg) => ({ url: `/serviceaccounts/${queryArg.serviceAccountId}`, method: 'DELETE' }), + invalidatesTags: ['service_accounts'], + }), + retrieveServiceAccount: build.query({ + query: (queryArg) => ({ url: `/serviceaccounts/${queryArg.serviceAccountId}` }), + providesTags: ['service_accounts'], + }), + updateServiceAccount: build.mutation({ + query: (queryArg) => ({ + url: `/serviceaccounts/${queryArg.serviceAccountId}`, + method: 'PATCH', + body: queryArg.updateServiceAccountForm, + }), + invalidatesTags: ['service_accounts'], + }), + listTokens: build.query({ + query: (queryArg) => ({ url: `/serviceaccounts/${queryArg.serviceAccountId}/tokens` }), + providesTags: ['service_accounts'], + }), + createToken: build.mutation({ + query: (queryArg) => ({ + url: `/serviceaccounts/${queryArg.serviceAccountId}/tokens`, + method: 'POST', + body: queryArg.addServiceAccountTokenCommand, + }), + invalidatesTags: ['service_accounts'], + }), + deleteToken: build.mutation({ + query: (queryArg) => ({ + url: `/serviceaccounts/${queryArg.serviceAccountId}/tokens/${queryArg.tokenId}`, + method: 'DELETE', + }), + invalidatesTags: ['service_accounts'], + }), + retrieveJwks: build.query({ + query: () => ({ url: `/signing-keys/keys` }), + providesTags: ['signing_keys'], + }), + getSharingOptions: build.query({ + query: () => ({ url: `/snapshot/shared-options` }), + providesTags: ['snapshots'], + }), + createDashboardSnapshot: build.mutation({ + query: (queryArg) => ({ url: `/snapshots`, method: 'POST', body: queryArg.createDashboardSnapshotCommand }), + invalidatesTags: ['dashboards', 'snapshots'], + }), + deleteDashboardSnapshotByDeleteKey: build.query< + DeleteDashboardSnapshotByDeleteKeyApiResponse, + DeleteDashboardSnapshotByDeleteKeyApiArg + >({ + query: (queryArg) => ({ url: `/snapshots-delete/${queryArg.deleteKey}` }), + providesTags: ['dashboards', 'snapshots'], + }), + deleteDashboardSnapshot: build.mutation({ + query: (queryArg) => ({ url: `/snapshots/${queryArg.key}`, method: 'DELETE' }), + invalidatesTags: ['dashboards', 'snapshots'], + }), + getDashboardSnapshot: build.query({ + query: (queryArg) => ({ url: `/snapshots/${queryArg.key}` }), + providesTags: ['dashboards', 'snapshots'], + }), + createTeam: build.mutation({ + query: (queryArg) => ({ url: `/teams`, method: 'POST', body: queryArg.createTeamCommand }), + invalidatesTags: ['teams'], + }), + searchTeams: build.query({ + query: (queryArg) => ({ + url: `/teams/search`, + params: { + page: queryArg.page, + perpage: queryArg.perpage, + name: queryArg.name, + query: queryArg.query, + accesscontrol: queryArg.accesscontrol, + sort: queryArg.sort, + }, + }), + providesTags: ['teams'], + }), + removeTeamGroupApiQuery: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}/groups`, + method: 'DELETE', + params: { + groupId: queryArg.groupId, + }, + }), + invalidatesTags: ['sync_team_groups', 'enterprise'], + }), + getTeamGroupsApi: build.query({ + query: (queryArg) => ({ url: `/teams/${queryArg.teamId}/groups` }), + providesTags: ['sync_team_groups', 'enterprise'], + }), + addTeamGroupApi: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}/groups`, + method: 'POST', + body: queryArg.teamGroupMapping, + }), + invalidatesTags: ['sync_team_groups', 'enterprise'], + }), + searchTeamGroups: build.query({ + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}/groups/search`, + params: { + page: queryArg.page, + perpage: queryArg.perpage, + query: queryArg.query, + name: queryArg.name, + }, + }), + providesTags: ['sync_team_groups', 'enterprise'], + }), + deleteTeamById: build.mutation({ + query: (queryArg) => ({ url: `/teams/${queryArg.teamId}`, method: 'DELETE' }), + invalidatesTags: ['teams'], + }), + getTeamById: build.query({ + query: (queryArg) => ({ url: `/teams/${queryArg.teamId}` }), + providesTags: ['teams'], + }), + updateTeam: build.mutation({ + query: (queryArg) => ({ url: `/teams/${queryArg.teamId}`, method: 'PUT', body: queryArg.updateTeamCommand }), + invalidatesTags: ['teams'], + }), + getTeamMembers: build.query({ + query: (queryArg) => ({ url: `/teams/${queryArg.teamId}/members` }), + providesTags: ['teams'], + }), + addTeamMember: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}/members`, + method: 'POST', + body: queryArg.addTeamMemberCommand, + }), + invalidatesTags: ['teams'], + }), + setTeamMemberships: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}/members`, + method: 'PUT', + body: queryArg.setTeamMembershipsCommand, + }), + invalidatesTags: ['teams'], + }), + removeTeamMember: build.mutation({ + query: (queryArg) => ({ url: `/teams/${queryArg.teamId}/members/${queryArg.userId}`, method: 'DELETE' }), + invalidatesTags: ['teams'], + }), + updateTeamMember: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}/members/${queryArg.userId}`, + method: 'PUT', + body: queryArg.updateTeamMemberCommand, + }), + invalidatesTags: ['teams'], + }), + getTeamPreferences: build.query({ + query: (queryArg) => ({ url: `/teams/${queryArg.teamId}/preferences` }), + providesTags: ['teams', 'preferences'], + }), + updateTeamPreferences: build.mutation({ + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}/preferences`, + method: 'PUT', + body: queryArg.updatePrefsCmd, + }), + invalidatesTags: ['teams', 'preferences'], + }), + getSignedInUser: build.query({ + query: () => ({ url: `/user` }), + providesTags: ['signed_in_user'], + }), + updateSignedInUser: build.mutation({ + query: (queryArg) => ({ url: `/user`, method: 'PUT', body: queryArg.updateUserCommand }), + invalidatesTags: ['signed_in_user'], + }), + getUserAuthTokens: build.query({ + query: () => ({ url: `/user/auth-tokens` }), + providesTags: ['signed_in_user'], + }), + updateUserEmail: build.query({ + query: () => ({ url: `/user/email/update` }), + providesTags: ['user'], + }), + clearHelpFlags: build.query({ + query: () => ({ url: `/user/helpflags/clear` }), + providesTags: ['signed_in_user'], + }), + setHelpFlag: build.mutation({ + query: (queryArg) => ({ url: `/user/helpflags/${queryArg.flagId}`, method: 'PUT' }), + invalidatesTags: ['signed_in_user'], + }), + getSignedInUserOrgList: build.query({ + query: () => ({ url: `/user/orgs` }), + providesTags: ['signed_in_user'], + }), + changeUserPassword: build.mutation({ + query: (queryArg) => ({ url: `/user/password`, method: 'PUT', body: queryArg.changeUserPasswordCommand }), + invalidatesTags: ['signed_in_user'], + }), + getUserPreferences: build.query({ + query: () => ({ url: `/user/preferences` }), + providesTags: ['signed_in_user', 'preferences'], + }), + patchUserPreferences: build.mutation({ + query: (queryArg) => ({ url: `/user/preferences`, method: 'PATCH', body: queryArg.patchPrefsCmd }), + invalidatesTags: ['signed_in_user', 'preferences'], + }), + updateUserPreferences: build.mutation({ + query: (queryArg) => ({ url: `/user/preferences`, method: 'PUT', body: queryArg.updatePrefsCmd }), + invalidatesTags: ['signed_in_user', 'preferences'], + }), + getUserQuotas: build.query({ + query: () => ({ url: `/user/quotas` }), + providesTags: ['quota', 'signed_in_user'], + }), + revokeUserAuthToken: build.mutation({ + query: (queryArg) => ({ url: `/user/revoke-auth-token`, method: 'POST', body: queryArg.revokeAuthTokenCmd }), + invalidatesTags: ['signed_in_user'], + }), + unstarDashboardByUid: build.mutation({ + query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'DELETE' }), + invalidatesTags: ['signed_in_user'], + }), + starDashboardByUid: build.mutation({ + query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'POST' }), + invalidatesTags: ['signed_in_user'], + }), + getSignedInUserTeamList: build.query({ + query: () => ({ url: `/user/teams` }), + providesTags: ['signed_in_user'], + }), + userSetUsingOrg: build.mutation({ + query: (queryArg) => ({ url: `/user/using/${queryArg.orgId}`, method: 'POST' }), + invalidatesTags: ['signed_in_user'], + }), + searchUsers: build.query({ + query: (queryArg) => ({ + url: `/users`, + params: { + perpage: queryArg.perpage, + page: queryArg.page, + }, + }), + providesTags: ['users'], + }), + getUserByLoginOrEmail: build.query({ + query: (queryArg) => ({ + url: `/users/lookup`, + params: { + loginOrEmail: queryArg.loginOrEmail, + }, + }), + providesTags: ['users'], + }), + searchUsersWithPaging: build.query({ + query: () => ({ url: `/users/search` }), + providesTags: ['users'], + }), + getUserById: build.query({ + query: (queryArg) => ({ url: `/users/${queryArg.userId}` }), + providesTags: ['users'], + }), + updateUser: build.mutation({ + query: (queryArg) => ({ url: `/users/${queryArg.userId}`, method: 'PUT', body: queryArg.updateUserCommand }), + invalidatesTags: ['users'], + }), + getUserOrgList: build.query({ + query: (queryArg) => ({ url: `/users/${queryArg.userId}/orgs` }), + providesTags: ['users'], + }), + getUserTeams: build.query({ + query: (queryArg) => ({ url: `/users/${queryArg.userId}/teams` }), + providesTags: ['users'], + }), + routeGetAlertRules: build.query({ + query: () => ({ url: `/v1/provisioning/alert-rules` }), + providesTags: ['provisioning'], + }), + routePostAlertRule: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/alert-rules`, + method: 'POST', + body: queryArg.provisionedAlertRule, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeGetAlertRulesExport: build.query({ + query: (queryArg) => ({ + url: `/v1/provisioning/alert-rules/export`, + params: { + download: queryArg.download, + format: queryArg.format, + folderUid: queryArg.folderUid, + group: queryArg.group, + ruleUid: queryArg.ruleUid, + }, + }), + providesTags: ['provisioning'], + }), + routeDeleteAlertRule: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/alert-rules/${queryArg.uid}`, + method: 'DELETE', + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeGetAlertRule: build.query({ + query: (queryArg) => ({ url: `/v1/provisioning/alert-rules/${queryArg.uid}` }), + providesTags: ['provisioning'], + }), + routePutAlertRule: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/alert-rules/${queryArg.uid}`, + method: 'PUT', + body: queryArg.provisionedAlertRule, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeGetAlertRuleExport: build.query({ + query: (queryArg) => ({ + url: `/v1/provisioning/alert-rules/${queryArg.uid}/export`, + params: { + download: queryArg.download, + format: queryArg.format, + }, + }), + providesTags: ['provisioning'], + }), + routeGetContactpoints: build.query({ + query: (queryArg) => ({ + url: `/v1/provisioning/contact-points`, + params: { + name: queryArg.name, + }, + }), + providesTags: ['provisioning'], + }), + routePostContactpoints: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/contact-points`, + method: 'POST', + body: queryArg.embeddedContactPoint, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeGetContactpointsExport: build.query< + RouteGetContactpointsExportApiResponse, + RouteGetContactpointsExportApiArg + >({ + query: (queryArg) => ({ + url: `/v1/provisioning/contact-points/export`, + params: { + download: queryArg.download, + format: queryArg.format, + decrypt: queryArg.decrypt, + name: queryArg.name, + }, + }), + providesTags: ['provisioning'], + }), + routeDeleteContactpoints: build.mutation({ + query: (queryArg) => ({ url: `/v1/provisioning/contact-points/${queryArg.uid}`, method: 'DELETE' }), + invalidatesTags: ['provisioning'], + }), + routePutContactpoint: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/contact-points/${queryArg.uid}`, + method: 'PUT', + body: queryArg.embeddedContactPoint, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeDeleteAlertRuleGroup: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/folder/${queryArg.folderUid}/rule-groups/${queryArg.group}`, + method: 'DELETE', + }), + invalidatesTags: ['provisioning'], + }), + routeGetAlertRuleGroup: build.query({ + query: (queryArg) => ({ url: `/v1/provisioning/folder/${queryArg.folderUid}/rule-groups/${queryArg.group}` }), + providesTags: ['provisioning'], + }), + routePutAlertRuleGroup: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/folder/${queryArg.folderUid}/rule-groups/${queryArg.group}`, + method: 'PUT', + body: queryArg.alertRuleGroup, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeGetAlertRuleGroupExport: build.query< + RouteGetAlertRuleGroupExportApiResponse, + RouteGetAlertRuleGroupExportApiArg + >({ + query: (queryArg) => ({ + url: `/v1/provisioning/folder/${queryArg.folderUid}/rule-groups/${queryArg.group}/export`, + params: { + download: queryArg.download, + format: queryArg.format, + }, + }), + providesTags: ['provisioning'], + }), + routeGetMuteTimings: build.query({ + query: () => ({ url: `/v1/provisioning/mute-timings` }), + providesTags: ['provisioning'], + }), + routePostMuteTiming: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/mute-timings`, + method: 'POST', + body: queryArg.muteTimeInterval, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeExportMuteTimings: build.query({ + query: (queryArg) => ({ + url: `/v1/provisioning/mute-timings/export`, + params: { + download: queryArg.download, + format: queryArg.format, + }, + }), + providesTags: ['provisioning'], + }), + routeDeleteMuteTiming: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/mute-timings/${queryArg.name}`, + method: 'DELETE', + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + params: { + version: queryArg.version, + }, + }), + invalidatesTags: ['provisioning'], + }), + routeGetMuteTiming: build.query({ + query: (queryArg) => ({ url: `/v1/provisioning/mute-timings/${queryArg.name}` }), + providesTags: ['provisioning'], + }), + routePutMuteTiming: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/mute-timings/${queryArg.name}`, + method: 'PUT', + body: queryArg.muteTimeInterval, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeExportMuteTiming: build.query({ + query: (queryArg) => ({ + url: `/v1/provisioning/mute-timings/${queryArg.name}/export`, + params: { + download: queryArg.download, + format: queryArg.format, + }, + }), + providesTags: ['provisioning'], + }), + routeResetPolicyTree: build.mutation({ + query: () => ({ url: `/v1/provisioning/policies`, method: 'DELETE' }), + invalidatesTags: ['provisioning'], + }), + routeGetPolicyTree: build.query({ + query: () => ({ url: `/v1/provisioning/policies` }), + providesTags: ['provisioning'], + }), + routePutPolicyTree: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/policies`, + method: 'PUT', + body: queryArg.route, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + routeGetPolicyTreeExport: build.query({ + query: () => ({ url: `/v1/provisioning/policies/export` }), + providesTags: ['provisioning'], + }), + routeGetTemplates: build.query({ + query: () => ({ url: `/v1/provisioning/templates` }), + providesTags: ['provisioning'], + }), + routeDeleteTemplate: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/templates/${queryArg.name}`, + method: 'DELETE', + params: { + version: queryArg.version, + }, + }), + invalidatesTags: ['provisioning'], + }), + routeGetTemplate: build.query({ + query: (queryArg) => ({ url: `/v1/provisioning/templates/${queryArg.name}` }), + providesTags: ['provisioning'], + }), + routePutTemplate: build.mutation({ + query: (queryArg) => ({ + url: `/v1/provisioning/templates/${queryArg.name}`, + method: 'PUT', + body: queryArg.notificationTemplateContent, + headers: { + 'X-Disable-Provenance': queryArg['X-Disable-Provenance'], + }, + }), + invalidatesTags: ['provisioning'], + }), + listAllProvidersSettings: build.query({ + query: () => ({ url: `/v1/sso-settings` }), + providesTags: ['sso_settings'], + }), + removeProviderSettings: build.mutation({ + query: (queryArg) => ({ url: `/v1/sso-settings/${queryArg.key}`, method: 'DELETE' }), + invalidatesTags: ['sso_settings'], + }), + getProviderSettings: build.query({ + query: (queryArg) => ({ url: `/v1/sso-settings/${queryArg.key}` }), + providesTags: ['sso_settings'], + }), + updateProviderSettings: build.mutation({ + query: (queryArg) => ({ url: `/v1/sso-settings/${queryArg.key}`, method: 'PUT', body: queryArg.body }), + invalidatesTags: ['sso_settings'], + }), + }), + overrideExisting: false, + }); +export { injectedRtkApi as generatedAPI }; +export type SearchResultApiResponse = /** status 200 (empty) */ SearchResult; +export type SearchResultApiArg = void; +export type ListRolesApiResponse = /** status 200 (empty) */ RoleDto[]; +export type ListRolesApiArg = { + delegatable?: boolean; + includeHidden?: boolean; +}; +export type CreateRoleApiResponse = /** status 201 (empty) */ RoleDto; +export type CreateRoleApiArg = { + createRoleForm: CreateRoleForm; +}; +export type DeleteRoleApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteRoleApiArg = { + force?: boolean; + global?: boolean; + roleUid: string; +}; +export type GetRoleApiResponse = /** status 200 (empty) */ RoleDto; +export type GetRoleApiArg = { + roleUid: string; +}; +export type UpdateRoleApiResponse = /** status 200 (empty) */ RoleDto; +export type UpdateRoleApiArg = { + roleUid: string; + updateRoleCommand: UpdateRoleCommand; +}; +export type GetRoleAssignmentsApiResponse = /** status 200 (empty) */ RoleAssignmentsDto; +export type GetRoleAssignmentsApiArg = { + roleUid: string; +}; +export type SetRoleAssignmentsApiResponse = /** status 200 (empty) */ RoleAssignmentsDto; +export type SetRoleAssignmentsApiArg = { + roleUid: string; + setRoleAssignmentsCommand: SetRoleAssignmentsCommand; +}; +export type GetAccessControlStatusApiResponse = /** status 200 (empty) */ Status; +export type GetAccessControlStatusApiArg = void; +export type ListTeamsRolesApiResponse = /** status 200 (empty) */ { + [key: string]: RoleDto[]; +}; +export type ListTeamsRolesApiArg = { + rolesSearchQuery: RolesSearchQuery; +}; +export type ListTeamRolesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type ListTeamRolesApiArg = { + teamId: number; +}; +export type AddTeamRoleApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AddTeamRoleApiArg = { + teamId: number; + addTeamRoleCommand: AddTeamRoleCommand; +}; +export type SetTeamRolesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SetTeamRolesApiArg = { + teamId: number; + setTeamRolesCommand: SetTeamRolesCommand; +}; +export type RemoveTeamRoleApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RemoveTeamRoleApiArg = { + roleUid: string; + teamId: number; +}; +export type ListUsersRolesApiResponse = /** status 200 (empty) */ { + [key: string]: RoleDto[]; +}; +export type ListUsersRolesApiArg = { + rolesSearchQuery: RolesSearchQuery; +}; +export type ListUserRolesApiResponse = /** status 200 (empty) */ RoleDto[]; +export type ListUserRolesApiArg = { + userId: number; +}; +export type AddUserRoleApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AddUserRoleApiArg = { + userId: number; + addUserRoleCommand: AddUserRoleCommand; +}; +export type SetUserRolesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SetUserRolesApiArg = { + userId: number; + setUserRolesCommand: SetUserRolesCommand; +}; +export type RemoveUserRoleApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RemoveUserRoleApiArg = { + /** A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment. */ + global?: boolean; + roleUid: string; + userId: number; +}; +export type GetResourceDescriptionApiResponse = /** status 200 (empty) */ Description; +export type GetResourceDescriptionApiArg = { + resource: string; +}; +export type GetResourcePermissionsApiResponse = /** status 200 (empty) */ ResourcePermissionDto[]; +export type GetResourcePermissionsApiArg = { + resource: string; + resourceId: string; +}; +export type SetResourcePermissionsApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SetResourcePermissionsApiArg = { + resource: string; + resourceId: string; + setPermissionsCommand: SetPermissionsCommand; +}; +export type SetResourcePermissionsForBuiltInRoleApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SetResourcePermissionsForBuiltInRoleApiArg = { + resource: string; + resourceId: string; + builtInRole: string; + setPermissionCommand: SetPermissionCommand; +}; +export type SetResourcePermissionsForTeamApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SetResourcePermissionsForTeamApiArg = { + resource: string; + resourceId: string; + teamId: number; + setPermissionCommand: SetPermissionCommand; +}; +export type SetResourcePermissionsForUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SetResourcePermissionsForUserApiArg = { + resource: string; + resourceId: string; + userId: number; + setPermissionCommand: SetPermissionCommand; +}; +export type GetSyncStatusApiResponse = /** status 200 (empty) */ ActiveSyncStatusDto; +export type GetSyncStatusApiArg = void; +export type ReloadLdapCfgApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type ReloadLdapCfgApiArg = void; +export type GetLdapStatusApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type GetLdapStatusApiArg = void; +export type PostSyncUserWithLdapApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type PostSyncUserWithLdapApiArg = { + userId: number; +}; +export type GetUserFromLdapApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type GetUserFromLdapApiArg = { + userName: string; +}; +export type AdminProvisioningReloadAccessControlApiResponse = /** status 202 AcceptedResponse */ ErrorResponseBody; +export type AdminProvisioningReloadAccessControlApiArg = void; +export type AdminProvisioningReloadDashboardsApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminProvisioningReloadDashboardsApiArg = void; +export type AdminProvisioningReloadDatasourcesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminProvisioningReloadDatasourcesApiArg = void; +export type AdminProvisioningReloadPluginsApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminProvisioningReloadPluginsApiArg = void; +export type AdminGetSettingsApiResponse = /** status 200 (empty) */ SettingsBag; +export type AdminGetSettingsApiArg = void; +export type AdminGetStatsApiResponse = /** status 200 (empty) */ AdminStats; +export type AdminGetStatsApiArg = void; +export type AdminCreateUserApiResponse = /** status 200 (empty) */ AdminCreateUserResponse; +export type AdminCreateUserApiArg = { + adminCreateUserForm: AdminCreateUserForm; +}; +export type AdminDeleteUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminDeleteUserApiArg = { + userId: number; +}; +export type AdminGetUserAuthTokensApiResponse = /** status 200 (empty) */ UserToken[]; +export type AdminGetUserAuthTokensApiArg = { + userId: number; +}; +export type AdminDisableUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminDisableUserApiArg = { + userId: number; +}; +export type AdminEnableUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminEnableUserApiArg = { + userId: number; +}; +export type AdminLogoutUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminLogoutUserApiArg = { + userId: number; +}; +export type AdminUpdateUserPasswordApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminUpdateUserPasswordApiArg = { + userId: number; + adminUpdateUserPasswordForm: AdminUpdateUserPasswordForm; +}; +export type AdminUpdateUserPermissionsApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminUpdateUserPermissionsApiArg = { + userId: number; + adminUpdateUserPermissionsForm: AdminUpdateUserPermissionsForm; +}; +export type GetUserQuotaApiResponse = /** status 200 (empty) */ QuotaDto[]; +export type GetUserQuotaApiArg = { + userId: number; +}; +export type UpdateUserQuotaApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateUserQuotaApiArg = { + quotaTarget: string; + userId: number; + updateQuotaCmd: UpdateQuotaCmd; +}; +export type AdminRevokeUserAuthTokenApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AdminRevokeUserAuthTokenApiArg = { + userId: number; + revokeAuthTokenCmd: RevokeAuthTokenCmd; +}; +export type GetAnnotationsApiResponse = /** status 200 (empty) */ Annotation[]; +export type GetAnnotationsApiArg = { + /** Find annotations created after specific epoch datetime in milliseconds. */ + from?: number; + /** Find annotations created before specific epoch datetime in milliseconds. */ + to?: number; + /** Limit response to annotations created by specific user. */ + userId?: number; + /** Find annotations for a specified alert rule by its ID. + deprecated: AlertID is deprecated and will be removed in future versions. Please use AlertUID instead. */ + alertId?: number; + /** Find annotations for a specified alert rule by its UID. */ + alertUid?: string; + /** Find annotations that are scoped to a specific dashboard */ + dashboardId?: number; + /** Find annotations that are scoped to a specific dashboard */ + dashboardUid?: string; + /** Find annotations that are scoped to a specific panel */ + panelId?: number; + /** Max limit for results returned. */ + limit?: number; + /** Use this to filter organization annotations. Organization annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. You can filter by multiple tags. */ + tags?: string[]; + /** Return alerts or user created annotations */ + type?: 'alert' | 'annotation'; + /** Match any or all tags */ + matchAny?: boolean; +}; +export type PostAnnotationApiResponse = /** status 200 (empty) */ { + /** ID Identifier of the created annotation. */ + id: number; + /** Message Message of the created annotation. */ + message: string; +}; +export type PostAnnotationApiArg = { + postAnnotationsCmd: PostAnnotationsCmd; +}; +export type PostGraphiteAnnotationApiResponse = /** status 200 (empty) */ { + /** ID Identifier of the created annotation. */ + id: number; + /** Message Message of the created annotation. */ + message: string; +}; +export type PostGraphiteAnnotationApiArg = { + postGraphiteAnnotationsCmd: PostGraphiteAnnotationsCmd; +}; +export type MassDeleteAnnotationsApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type MassDeleteAnnotationsApiArg = { + massDeleteAnnotationsCmd: MassDeleteAnnotationsCmd; +}; +export type GetAnnotationTagsApiResponse = + /** status 200 (empty) */ GetAnnotationTagsResponseIsAResponseStructForFindTagsResult; +export type GetAnnotationTagsApiArg = { + /** Tag is a string that you can use to filter tags. */ + tag?: string; + /** Max limit for results returned. */ + limit?: string; +}; +export type DeleteAnnotationByIdApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteAnnotationByIdApiArg = { + annotationId: string; +}; +export type GetAnnotationByIdApiResponse = /** status 200 (empty) */ Annotation; +export type GetAnnotationByIdApiArg = { + annotationId: string; +}; +export type PatchAnnotationApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type PatchAnnotationApiArg = { + annotationId: string; + patchAnnotationsCmd: PatchAnnotationsCmd; +}; +export type UpdateAnnotationApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateAnnotationApiArg = { + annotationId: string; + updateAnnotationsCmd: UpdateAnnotationsCmd; +}; +export type ListDevicesApiResponse = /** status 200 (empty) */ DeviceDto[]; +export type ListDevicesApiArg = void; +export type SearchDevicesApiResponse = /** status 200 (empty) */ SearchDeviceQueryResult; +export type SearchDevicesApiArg = void; +export type GetSessionListApiResponse = /** status 200 (empty) */ CloudMigrationSessionListResponseDto; +export type GetSessionListApiArg = void; +export type CreateSessionApiResponse = /** status 200 (empty) */ CloudMigrationSessionResponseDto; +export type CreateSessionApiArg = { + cloudMigrationSessionRequestDto: CloudMigrationSessionRequestDto; +}; +export type DeleteSessionApiResponse = unknown; +export type DeleteSessionApiArg = { + /** UID of a migration session */ + uid: string; +}; +export type GetSessionApiResponse = /** status 200 (empty) */ CloudMigrationSessionResponseDto; +export type GetSessionApiArg = { + /** UID of a migration session */ + uid: string; +}; +export type CreateSnapshotApiResponse = /** status 200 (empty) */ CreateSnapshotResponseDto; +export type CreateSnapshotApiArg = { + /** UID of a session */ + uid: string; + createSnapshotRequestDto: CreateSnapshotRequestDto; +}; +export type GetSnapshotApiResponse = /** status 200 (empty) */ GetSnapshotResponseDto; +export type GetSnapshotApiArg = { + /** ResultPage is used for pagination with ResultLimit */ + resultPage?: number; + /** Max limit for snapshot results returned. */ + resultLimit?: number; + /** ResultSortColumn can be used to override the default system sort. Valid values are "name", "resource_type", and "status". */ + resultSortColumn?: string; + /** ResultSortOrder is used with ResultSortColumn. Valid values are ASC and DESC. */ + resultSortOrder?: string; + /** ErrorsOnly is used to only return resources with error statuses */ + errorsOnly?: boolean; + /** Session UID of a session */ + uid: string; + /** UID of a snapshot */ + snapshotUid: string; +}; +export type CancelSnapshotApiResponse = unknown; +export type CancelSnapshotApiArg = { + /** Session UID of a session */ + uid: string; + /** UID of a snapshot */ + snapshotUid: string; +}; +export type UploadSnapshotApiResponse = unknown; +export type UploadSnapshotApiArg = { + /** Session UID of a session */ + uid: string; + /** UID of a snapshot */ + snapshotUid: string; +}; +export type GetShapshotListApiResponse = /** status 200 (empty) */ SnapshotListResponseDto; +export type GetShapshotListApiArg = { + /** Page is used for pagination with limit */ + page?: number; + /** Max limit for results returned. */ + limit?: number; + /** Session UID of a session */ + uid: string; + /** Sort with value latest to return results sorted in descending order. */ + sort?: string; +}; +export type GetResourceDependenciesApiResponse = /** status 200 (empty) */ ResourceDependenciesResponseDto; +export type GetResourceDependenciesApiArg = void; +export type GetCloudMigrationTokenApiResponse = /** status 200 (empty) */ GetAccessTokenResponseDto; +export type GetCloudMigrationTokenApiArg = void; +export type CreateCloudMigrationTokenApiResponse = /** status 200 (empty) */ CreateAccessTokenResponseDto; +export type CreateCloudMigrationTokenApiArg = void; +export type DeleteCloudMigrationTokenApiResponse = unknown; +export type DeleteCloudMigrationTokenApiArg = { + /** UID of a cloud migration token */ + uid: string; +}; +export type RouteConvertPrometheusCortexGetRulesApiResponse = unknown; +export type RouteConvertPrometheusCortexGetRulesApiArg = void; +export type RouteConvertPrometheusCortexPostRuleGroupsApiResponse = + /** status 202 ConvertPrometheusResponse */ ConvertPrometheusResponse; +export type RouteConvertPrometheusCortexPostRuleGroupsApiArg = void; +export type RouteConvertPrometheusCortexDeleteNamespaceApiResponse = + /** status 202 ConvertPrometheusResponse */ ConvertPrometheusResponse; +export type RouteConvertPrometheusCortexDeleteNamespaceApiArg = { + namespaceTitle: string; +}; +export type RouteConvertPrometheusCortexGetNamespaceApiResponse = unknown; +export type RouteConvertPrometheusCortexGetNamespaceApiArg = { + namespaceTitle: string; +}; +export type RouteConvertPrometheusCortexPostRuleGroupApiResponse = + /** status 202 ConvertPrometheusResponse */ ConvertPrometheusResponse; +export type RouteConvertPrometheusCortexPostRuleGroupApiArg = { + namespaceTitle: string; + 'x-grafana-alerting-datasource-uid'?: string; + 'x-grafana-alerting-recording-rules-paused'?: boolean; + 'x-grafana-alerting-alert-rules-paused'?: boolean; + 'x-grafana-alerting-target-datasource-uid'?: string; + 'x-grafana-alerting-folder-uid'?: string; + 'x-grafana-alerting-notification-receiver'?: string; + prometheusRuleGroup: PrometheusRuleGroup; +}; +export type RouteConvertPrometheusCortexDeleteRuleGroupApiResponse = + /** status 202 ConvertPrometheusResponse */ ConvertPrometheusResponse; +export type RouteConvertPrometheusCortexDeleteRuleGroupApiArg = { + namespaceTitle: string; + group: string; +}; +export type RouteConvertPrometheusCortexGetRuleGroupApiResponse = unknown; +export type RouteConvertPrometheusCortexGetRuleGroupApiArg = { + namespaceTitle: string; + group: string; +}; +export type RouteConvertPrometheusGetRulesApiResponse = unknown; +export type RouteConvertPrometheusGetRulesApiArg = void; +export type RouteConvertPrometheusPostRuleGroupsApiResponse = + /** status 202 ConvertPrometheusResponse */ ConvertPrometheusResponse; +export type RouteConvertPrometheusPostRuleGroupsApiArg = void; +export type RouteConvertPrometheusDeleteNamespaceApiResponse = + /** status 202 ConvertPrometheusResponse */ ConvertPrometheusResponse; +export type RouteConvertPrometheusDeleteNamespaceApiArg = { + namespaceTitle: string; +}; +export type RouteConvertPrometheusGetNamespaceApiResponse = unknown; +export type RouteConvertPrometheusGetNamespaceApiArg = { + namespaceTitle: string; +}; +export type RouteConvertPrometheusPostRuleGroupApiResponse = + /** status 202 ConvertPrometheusResponse */ ConvertPrometheusResponse; +export type RouteConvertPrometheusPostRuleGroupApiArg = { + namespaceTitle: string; + 'x-grafana-alerting-datasource-uid'?: string; + 'x-grafana-alerting-recording-rules-paused'?: boolean; + 'x-grafana-alerting-alert-rules-paused'?: boolean; + 'x-grafana-alerting-target-datasource-uid'?: string; + 'x-grafana-alerting-folder-uid'?: string; + 'x-grafana-alerting-notification-receiver'?: string; + prometheusRuleGroup: PrometheusRuleGroup; +}; +export type RouteConvertPrometheusDeleteRuleGroupApiResponse = + /** status 202 ConvertPrometheusResponse */ ConvertPrometheusResponse; +export type RouteConvertPrometheusDeleteRuleGroupApiArg = { + namespaceTitle: string; + group: string; +}; +export type RouteConvertPrometheusGetRuleGroupApiResponse = unknown; +export type RouteConvertPrometheusGetRuleGroupApiArg = { + namespaceTitle: string; + group: string; +}; +export type SearchDashboardSnapshotsApiResponse = /** status 200 (empty) */ DashboardSnapshotDto[]; +export type SearchDashboardSnapshotsApiArg = { + /** Search Query */ + query?: string; + /** Limit the number of returned results */ + limit?: number; +}; +export type CalculateDashboardDiffApiResponse = /** status 200 (empty) */ number[]; +export type CalculateDashboardDiffApiArg = { + body: { + base?: CalculateDiffTarget; + /** The type of diff to return + Description: + `basic` + `json` */ + diffType?: 'basic' | 'json'; + new?: CalculateDiffTarget; + }; +}; +export type PostDashboardApiResponse = /** status 200 (empty) */ { + /** FolderUID The unique identifier (uid) of the folder the dashboard belongs to. */ + folderUid?: string; + /** ID The unique identifier (id) of the created/updated dashboard. */ + id: number; + /** Status status of the response. */ + status: string; + /** Slug The slug of the dashboard. */ + title: string; + /** UID The unique identifier (uid) of the created/updated dashboard. */ + uid: string; + /** URL The relative URL for accessing the created/updated dashboard. */ + url: string; + /** Version The version of the dashboard. */ + version: number; +}; +export type PostDashboardApiArg = { + saveDashboardCommand: SaveDashboardCommand; +}; +export type GetHomeDashboardApiResponse = /** status 200 (empty) */ GetHomeDashboardResponse; +export type GetHomeDashboardApiArg = void; +export type ImportDashboardApiResponse = + /** status 200 (empty) */ ImportDashboardResponseResponseObjectReturnedWhenImportingADashboard; +export type ImportDashboardApiArg = { + importDashboardRequest: ImportDashboardRequestRequestObjectForImportingADashboard; +}; +export type InterpolateDashboardApiResponse = /** status 200 (empty) */ any; +export type InterpolateDashboardApiArg = void; +export type ListPublicDashboardsApiResponse = /** status 200 (empty) */ PublicDashboardListResponseWithPagination; +export type ListPublicDashboardsApiArg = void; +export type GetDashboardTagsApiResponse = /** status 200 (empty) */ DashboardTagCloudItem[]; +export type GetDashboardTagsApiArg = void; +export type GetPublicDashboardApiResponse = /** status 200 (empty) */ PublicDashboard; +export type GetPublicDashboardApiArg = { + dashboardUid: string; +}; +export type CreatePublicDashboardApiResponse = /** status 200 (empty) */ PublicDashboard; +export type CreatePublicDashboardApiArg = { + dashboardUid: string; + publicDashboardDto: PublicDashboardDto; +}; +export type DeletePublicDashboardApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeletePublicDashboardApiArg = { + dashboardUid: string; + uid: string; +}; +export type UpdatePublicDashboardApiResponse = /** status 200 (empty) */ PublicDashboard; +export type UpdatePublicDashboardApiArg = { + dashboardUid: string; + uid: string; + publicDashboardDto: PublicDashboardDto; +}; +export type DeleteDashboardByUidApiResponse = /** status 200 (empty) */ { + /** Message Message of the deleted dashboard. */ + message: string; + /** Title Title of the deleted dashboard. */ + title: string; + /** UID Identifier of the deleted dashboard. */ + uid: string; +}; +export type DeleteDashboardByUidApiArg = { + uid: string; +}; +export type GetDashboardByUidApiResponse = /** status 200 (empty) */ DashboardFullWithMeta; +export type GetDashboardByUidApiArg = { + uid: string; +}; +export type GetDashboardPermissionsListByUidApiResponse = /** status 200 (empty) */ DashboardAclInfoDto[]; +export type GetDashboardPermissionsListByUidApiArg = { + uid: string; +}; +export type UpdateDashboardPermissionsByUidApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateDashboardPermissionsByUidApiArg = { + uid: string; + updateDashboardAclCommand: UpdateDashboardAclCommand; +}; +export type RestoreDashboardVersionByUidApiResponse = /** status 200 (empty) */ { + /** FolderUID The unique identifier (uid) of the folder the dashboard belongs to. */ + folderUid?: string; + /** ID The unique identifier (id) of the created/updated dashboard. */ + id: number; + /** Status status of the response. */ + status: string; + /** Slug The slug of the dashboard. */ + title: string; + /** UID The unique identifier (uid) of the created/updated dashboard. */ + uid: string; + /** URL The relative URL for accessing the created/updated dashboard. */ + url: string; + /** Version The version of the dashboard. */ + version: number; +}; +export type RestoreDashboardVersionByUidApiArg = { + uid: string; + restoreDashboardVersionCommand: RestoreDashboardVersionCommand; +}; +export type GetDashboardVersionsByUidApiResponse = /** status 200 (empty) */ DashboardVersionResponseMeta; +export type GetDashboardVersionsByUidApiArg = { + uid: string; + /** Maximum number of results to return */ + limit?: number; + /** Version to start from when returning queries */ + start?: number; +}; +export type GetDashboardVersionByUidApiResponse = /** status 200 (empty) */ DashboardVersionMeta; +export type GetDashboardVersionByUidApiArg = { + dashboardVersionId: number; + uid: string; +}; +export type GetDataSourcesApiResponse = /** status 200 (empty) */ DataSourceList; +export type GetDataSourcesApiArg = void; +export type AddDataSourceApiResponse = /** status 200 (empty) */ { + datasource: DataSource; + /** ID Identifier of the new data source. */ + id: number; + /** Message Message of the deleted dashboard. */ + message: string; + /** Name of the new data source. */ + name: string; +}; +export type AddDataSourceApiArg = { + addDataSourceCommand: AddDataSourceCommand; +}; +export type GetCorrelationsApiResponse = /** status 200 (empty) */ Correlation[]; +export type GetCorrelationsApiArg = { + /** Limit the maximum number of correlations to return per page */ + limit?: number; + /** Page index for starting fetching correlations */ + page?: number; + /** Source datasource UID filter to be applied to correlations */ + sourceUid?: string[]; +}; +export type GetDataSourceIdByNameApiResponse = /** status 200 (empty) */ { + /** ID Identifier of the data source. */ + id: number; +}; +export type GetDataSourceIdByNameApiArg = { + name: string; +}; +export type DeleteDataSourceByNameApiResponse = /** status 200 (empty) */ { + /** ID Identifier of the deleted data source. */ + id: number; + /** Message Message of the deleted dashboard. */ + message: string; +}; +export type DeleteDataSourceByNameApiArg = { + name: string; +}; +export type GetDataSourceByNameApiResponse = /** status 200 (empty) */ DataSource; +export type GetDataSourceByNameApiArg = { + name: string; +}; +export type DatasourceProxyDeleteByUiDcallsApiResponse = unknown; +export type DatasourceProxyDeleteByUiDcallsApiArg = { + uid: string; + datasourceProxyRoute: string; +}; +export type DatasourceProxyGetByUiDcallsApiResponse = unknown; +export type DatasourceProxyGetByUiDcallsApiArg = { + datasourceProxyRoute: string; + uid: string; +}; +export type DatasourceProxyPostByUiDcallsApiResponse = unknown; +export type DatasourceProxyPostByUiDcallsApiArg = { + datasourceProxyRoute: string; + uid: string; + body: any; +}; +export type GetCorrelationsBySourceUidApiResponse = /** status 200 (empty) */ Correlation[]; +export type GetCorrelationsBySourceUidApiArg = { + sourceUid: string; +}; +export type CreateCorrelationApiResponse = /** status 200 (empty) */ CreateCorrelationResponseBody; +export type CreateCorrelationApiArg = { + sourceUid: string; + createCorrelationCommand: CreateCorrelationCommand; +}; +export type GetCorrelationApiResponse = /** status 200 (empty) */ Correlation; +export type GetCorrelationApiArg = { + sourceUid: string; + correlationUid: string; +}; +export type UpdateCorrelationApiResponse = /** status 200 (empty) */ UpdateCorrelationResponseBody; +export type UpdateCorrelationApiArg = { + sourceUid: string; + correlationUid: string; + updateCorrelationCommand: UpdateCorrelationCommand; +}; +export type DeleteDataSourceByUidApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteDataSourceByUidApiArg = { + uid: string; +}; +export type GetDataSourceByUidApiResponse = /** status 200 (empty) */ DataSource; +export type GetDataSourceByUidApiArg = { + uid: string; +}; +export type UpdateDataSourceByUidApiResponse = /** status 200 (empty) */ { + datasource: DataSource; + /** ID Identifier of the new data source. */ + id: number; + /** Message Message of the deleted dashboard. */ + message: string; + /** Name of the new data source. */ + name: string; +}; +export type UpdateDataSourceByUidApiArg = { + uid: string; + updateDataSourceCommand: UpdateDataSourceCommand; +}; +export type DeleteCorrelationApiResponse = /** status 200 (empty) */ DeleteCorrelationResponseBody; +export type DeleteCorrelationApiArg = { + uid: string; + correlationUid: string; +}; +export type CheckDatasourceHealthWithUidApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type CheckDatasourceHealthWithUidApiArg = { + uid: string; +}; +export type GetTeamLbacRulesApiApiResponse = /** status 200 (empty) */ TeamLbacRules; +export type GetTeamLbacRulesApiApiArg = { + uid: string; +}; +export type UpdateTeamLbacRulesApiApiResponse = /** status 200 (empty) */ { + id?: number; + message?: string; + name?: string; + rules?: TeamLbacRule[]; + uid?: string; +}; +export type UpdateTeamLbacRulesApiApiArg = { + uid: string; + updateTeamLbacCommand: UpdateTeamLbacCommand; +}; +export type CallDatasourceResourceWithUidApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type CallDatasourceResourceWithUidApiArg = { + datasourceProxyRoute: string; + uid: string; +}; +export type GetDataSourceCacheConfigApiResponse = /** status 200 CacheConfigResponse */ CacheConfigResponse; +export type GetDataSourceCacheConfigApiArg = { + dataSourceUid: string; +}; +export type SetDataSourceCacheConfigApiResponse = /** status 200 CacheConfigResponse */ CacheConfigResponse; +export type SetDataSourceCacheConfigApiArg = { + dataSourceUid: string; + cacheConfigSetter: CacheConfigSetter; +}; +export type CleanDataSourceCacheApiResponse = /** status 200 CacheConfigResponse */ CacheConfigResponse; +export type CleanDataSourceCacheApiArg = { + dataSourceUid: string; +}; +export type DisableDataSourceCacheApiResponse = /** status 200 CacheConfigResponse */ CacheConfigResponse; +export type DisableDataSourceCacheApiArg = { + dataSourceUid: string; +}; +export type EnableDataSourceCacheApiResponse = /** status 200 CacheConfigResponse */ CacheConfigResponse; +export type EnableDataSourceCacheApiArg = { + dataSourceUid: string; +}; +export type QueryMetricsWithExpressionsApiResponse = /** status 200 (empty) */ + | QueryDataResponseContainsTheResultsFromAQueryDataRequest + | /** status 207 (empty) */ QueryDataResponseContainsTheResultsFromAQueryDataRequest; +export type QueryMetricsWithExpressionsApiArg = { + metricRequest: MetricRequest; +}; +export type GetFoldersApiResponse = /** status 200 (empty) */ FolderSearchHit[]; +export type GetFoldersApiArg = { + /** Limit the maximum number of folders to return */ + limit?: number; + /** Page index for starting fetching folders */ + page?: number; + /** The parent folder UID */ + parentUid?: string; + /** Set to `Edit` to return folders that the user can edit */ + permission?: 'Edit' | 'View'; +}; +export type CreateFolderApiResponse = /** status 200 (empty) */ Folder; +export type CreateFolderApiArg = { + createFolderCommand: CreateFolderCommand; +}; +export type DeleteFolderApiResponse = /** status 200 (empty) */ { + /** ID Identifier of the deleted folder. */ + id: number; + /** Message Message of the deleted folder. */ + message: string; + /** Title of the deleted folder. */ + title: string; +}; +export type DeleteFolderApiArg = { + folderUid: string; + /** If `true` any Grafana 8 Alerts under this folder will be deleted. + Set to `false` so that the request will fail if the folder contains any Grafana 8 Alerts. */ + forceDeleteRules?: boolean; +}; +export type GetFolderByUidApiResponse = /** status 200 (empty) */ Folder; +export type GetFolderByUidApiArg = { + folderUid: string; +}; +export type UpdateFolderApiResponse = /** status 200 (empty) */ Folder; +export type UpdateFolderApiArg = { + folderUid: string; + /** To change the unique identifier (uid), provide another one. + To overwrite an existing folder with newer version, set `overwrite` to `true`. + Provide the current version to safelly update the folder: if the provided version differs from the stored one the request will fail, unless `overwrite` is `true`. */ + updateFolderCommand: UpdateFolderCommand; +}; +export type GetFolderDescendantCountsApiResponse = /** status 200 (empty) */ DescendantCounts; +export type GetFolderDescendantCountsApiArg = { + folderUid: string; +}; +export type MoveFolderApiResponse = /** status 200 (empty) */ Folder; +export type MoveFolderApiArg = { + folderUid: string; + moveFolderCommand: MoveFolderCommand; +}; +export type GetFolderPermissionListApiResponse = /** status 200 (empty) */ DashboardAclInfoDto[]; +export type GetFolderPermissionListApiArg = { + folderUid: string; +}; +export type UpdateFolderPermissionsApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateFolderPermissionsApiArg = { + folderUid: string; + updateDashboardAclCommand: UpdateDashboardAclCommand; +}; +export type GetMappedGroupsApiResponse = /** status 200 (empty) */ GetGroupsResponse; +export type GetMappedGroupsApiArg = void; +export type DeleteGroupMappingsApiResponse = + /** status 204 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteGroupMappingsApiArg = { + groupId: string; +}; +export type CreateGroupMappingsApiResponse = /** status 201 (empty) */ MessageResponse; +export type CreateGroupMappingsApiArg = { + groupId: string; + groupAttributes: GroupAttributes; +}; +export type UpdateGroupMappingsApiResponse = /** status 201 (empty) */ MessageResponse; +export type UpdateGroupMappingsApiArg = { + groupId: string; + groupAttributes: GroupAttributes; +}; +export type GetGroupRolesApiResponse = /** status 200 (empty) */ RoleDto[]; +export type GetGroupRolesApiArg = { + groupId: string; +}; +export type GetHealthApiResponse = /** status 200 healthResponse */ HealthResponse; +export type GetHealthApiArg = void; +export type GetLibraryElementsApiResponse = + /** status 200 (empty) */ LibraryElementSearchResponseIsAResponseStructForLibraryElementSearchResult; +export type GetLibraryElementsApiArg = { + /** Part of the name or description searched for. */ + searchString?: string; + /** Kind of element to search for. */ + kind?: 1; + /** Sort order of elements. */ + sortDirection?: 'alpha-asc' | 'alpha-desc'; + /** A comma separated list of types to filter the elements by */ + typeFilter?: string; + /** Element UID to exclude from search results. */ + excludeUid?: string; + /** A comma separated list of folder ID(s) to filter the elements by. */ + folderFilter?: string; + /** The number of results per page. */ + perPage?: number; + /** The page for a set of records, given that only perPage records are returned at a time. Numbering starts at 1. */ + page?: number; +}; +export type CreateLibraryElementApiResponse = + /** status 200 (empty) */ LibraryElementResponseIsAResponseStructForLibraryElementDto; +export type CreateLibraryElementApiArg = { + createLibraryElementCommand: CreateLibraryElementCommand; +}; +export type GetLibraryElementByNameApiResponse = + /** status 200 (empty) */ LibraryElementArrayResponseIsAResponseStructForAnArrayOfLibraryElementDto; +export type GetLibraryElementByNameApiArg = { + libraryElementName: string; +}; +export type DeleteLibraryElementByUidApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteLibraryElementByUidApiArg = { + libraryElementUid: string; +}; +export type GetLibraryElementByUidApiResponse = + /** status 200 (empty) */ LibraryElementResponseIsAResponseStructForLibraryElementDto; +export type GetLibraryElementByUidApiArg = { + libraryElementUid: string; +}; +export type UpdateLibraryElementApiResponse = + /** status 200 (empty) */ LibraryElementResponseIsAResponseStructForLibraryElementDto; +export type UpdateLibraryElementApiArg = { + libraryElementUid: string; + patchLibraryElementCommand: PatchLibraryElementCommand; +}; +export type GetLibraryElementConnectionsApiResponse = + /** status 200 (empty) */ LibraryElementConnectionsResponseIsAResponseStructForAnArrayOfLibraryElementConnectionDto; +export type GetLibraryElementConnectionsApiArg = { + libraryElementUid: string; +}; +export type GetStatusApiResponse = unknown; +export type GetStatusApiArg = void; +export type RefreshLicenseStatsApiResponse = /** status 200 (empty) */ ActiveUserStats; +export type RefreshLicenseStatsApiArg = void; +export type DeleteLicenseTokenApiResponse = /** status 202 AcceptedResponse */ ErrorResponseBody; +export type DeleteLicenseTokenApiArg = { + deleteTokenCommand: DeleteTokenCommand; +}; +export type GetLicenseTokenApiResponse = /** status 200 (empty) */ Token; +export type GetLicenseTokenApiArg = void; +export type PostLicenseTokenApiResponse = /** status 200 (empty) */ Token; +export type PostLicenseTokenApiArg = { + deleteTokenCommand: DeleteTokenCommand; +}; +export type PostRenewLicenseTokenApiResponse = unknown; +export type PostRenewLicenseTokenApiArg = { + body: object; +}; +export type GetSamlLogoutApiResponse = unknown; +export type GetSamlLogoutApiArg = void; +export type GetCurrentOrgApiResponse = /** status 200 (empty) */ OrgDetailsDto; +export type GetCurrentOrgApiArg = void; +export type UpdateCurrentOrgApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateCurrentOrgApiArg = { + updateOrgForm: UpdateOrgForm; +}; +export type UpdateCurrentOrgAddressApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateCurrentOrgAddressApiArg = { + updateOrgAddressForm: UpdateOrgAddressForm; +}; +export type GetPendingOrgInvitesApiResponse = /** status 200 (empty) */ TempUserDto[]; +export type GetPendingOrgInvitesApiArg = void; +export type AddOrgInviteApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AddOrgInviteApiArg = { + addInviteForm: AddInviteForm; +}; +export type RevokeInviteApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RevokeInviteApiArg = { + invitationCode: string; +}; +export type GetOrgPreferencesApiResponse = /** status 200 (empty) */ PreferencesSpec; +export type GetOrgPreferencesApiArg = void; +export type PatchOrgPreferencesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type PatchOrgPreferencesApiArg = { + patchPrefsCmd: PatchPrefsCmd; +}; +export type UpdateOrgPreferencesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateOrgPreferencesApiArg = { + updatePrefsCmd: UpdatePrefsCmd; +}; +export type GetCurrentOrgQuotaApiResponse = /** status 200 (empty) */ QuotaDto[]; +export type GetCurrentOrgQuotaApiArg = void; +export type GetOrgUsersForCurrentOrgApiResponse = /** status 200 (empty) */ OrgUserDto[]; +export type GetOrgUsersForCurrentOrgApiArg = { + query?: string; + limit?: number; +}; +export type AddOrgUserToCurrentOrgApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AddOrgUserToCurrentOrgApiArg = { + addOrgUserCommand: AddOrgUserCommand; +}; +export type GetOrgUsersForCurrentOrgLookupApiResponse = /** status 200 (empty) */ UserLookupDto[]; +export type GetOrgUsersForCurrentOrgLookupApiArg = { + query?: string; + limit?: number; +}; +export type RemoveOrgUserForCurrentOrgApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RemoveOrgUserForCurrentOrgApiArg = { + userId: number; +}; +export type UpdateOrgUserForCurrentOrgApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateOrgUserForCurrentOrgApiArg = { + userId: number; + updateOrgUserCommand: UpdateOrgUserCommand; +}; +export type SearchOrgsApiResponse = /** status 200 (empty) */ OrgDto[]; +export type SearchOrgsApiArg = { + page?: number; + /** Number of items per page + The totalCount field in the response can be used for pagination list E.g. if totalCount is equal to 100 teams and the perpage parameter is set to 10 then there are 10 pages of teams. */ + perpage?: number; + name?: string; + /** If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded. */ + query?: string; +}; +export type CreateOrgApiResponse = /** status 200 (empty) */ { + /** Message Message of the created org. */ + message: string; + /** ID Identifier of the created org. */ + orgId: number; +}; +export type CreateOrgApiArg = { + createOrgCommand: CreateOrgCommand; +}; +export type GetOrgByNameApiResponse = /** status 200 (empty) */ OrgDetailsDto; +export type GetOrgByNameApiArg = { + orgName: string; +}; +export type DeleteOrgByIdApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteOrgByIdApiArg = { + orgId: number; +}; +export type GetOrgByIdApiResponse = /** status 200 (empty) */ OrgDetailsDto; +export type GetOrgByIdApiArg = { + orgId: number; +}; +export type UpdateOrgApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateOrgApiArg = { + orgId: number; + updateOrgForm: UpdateOrgForm; +}; +export type UpdateOrgAddressApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateOrgAddressApiArg = { + orgId: number; + updateOrgAddressForm: UpdateOrgAddressForm; +}; +export type GetOrgQuotaApiResponse = /** status 200 (empty) */ QuotaDto[]; +export type GetOrgQuotaApiArg = { + orgId: number; +}; +export type UpdateOrgQuotaApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateOrgQuotaApiArg = { + quotaTarget: string; + orgId: number; + updateQuotaCmd: UpdateQuotaCmd; +}; +export type GetOrgUsersApiResponse = /** status 200 (empty) */ OrgUserDto[]; +export type GetOrgUsersApiArg = { + orgId: number; +}; +export type AddOrgUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AddOrgUserApiArg = { + orgId: number; + addOrgUserCommand: AddOrgUserCommand; +}; +export type SearchOrgUsersApiResponse = /** status 200 (empty) */ SearchOrgUsersQueryResult; +export type SearchOrgUsersApiArg = { + orgId: number; +}; +export type RemoveOrgUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RemoveOrgUserApiArg = { + orgId: number; + userId: number; +}; +export type UpdateOrgUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateOrgUserApiArg = { + orgId: number; + userId: number; + updateOrgUserCommand: UpdateOrgUserCommand; +}; +export type SearchPlaylistsApiResponse = /** status 200 (empty) */ Playlists; +export type SearchPlaylistsApiArg = { + query?: string; + /** in:limit */ + limit?: number; +}; +export type CreatePlaylistApiResponse = /** status 200 (empty) */ Playlist; +export type CreatePlaylistApiArg = { + createPlaylistCommand: CreatePlaylistCommand; +}; +export type DeletePlaylistApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeletePlaylistApiArg = { + uid: string; +}; +export type GetPlaylistApiResponse = /** status 200 (empty) */ PlaylistDto; +export type GetPlaylistApiArg = { + uid: string; +}; +export type UpdatePlaylistApiResponse = /** status 200 (empty) */ PlaylistDto; +export type UpdatePlaylistApiArg = { + uid: string; + updatePlaylistCommand: UpdatePlaylistCommand; +}; +export type GetPlaylistItemsApiResponse = /** status 200 (empty) */ PlaylistItemDto[]; +export type GetPlaylistItemsApiArg = { + uid: string; +}; +export type ViewPublicDashboardApiResponse = /** status 200 (empty) */ DashboardFullWithMeta; +export type ViewPublicDashboardApiArg = { + accessToken: string; +}; +export type GetPublicAnnotationsApiResponse = /** status 200 (empty) */ AnnotationEvent[]; +export type GetPublicAnnotationsApiArg = { + accessToken: string; +}; +export type QueryPublicDashboardApiResponse = + /** status 200 (empty) */ QueryDataResponseContainsTheResultsFromAQueryDataRequest; +export type QueryPublicDashboardApiArg = { + accessToken: string; + panelId: number; +}; +export type SearchQueriesApiResponse = /** status 200 (empty) */ QueryHistorySearchResponse; +export type SearchQueriesApiArg = { + /** List of data source UIDs to search for */ + datasourceUid?: string[]; + /** Text inside query or comments that is searched for */ + searchString?: string; + /** Flag indicating if only starred queries should be returned */ + onlyStarred?: boolean; + /** Sort method */ + sort?: 'time-desc' | 'time-asc'; + /** Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size. */ + page?: number; + /** Limit the number of returned results */ + limit?: number; + /** From range for the query history search */ + from?: number; + /** To range for the query history search */ + to?: number; +}; +export type CreateQueryApiResponse = /** status 200 (empty) */ QueryHistoryResponse; +export type CreateQueryApiArg = { + createQueryInQueryHistoryCommand: CreateQueryInQueryHistoryCommand; +}; +export type UnstarQueryApiResponse = /** status 200 (empty) */ QueryHistoryResponse; +export type UnstarQueryApiArg = { + queryHistoryUid: string; +}; +export type StarQueryApiResponse = /** status 200 (empty) */ QueryHistoryResponse; +export type StarQueryApiArg = { + queryHistoryUid: string; +}; +export type DeleteQueryApiResponse = /** status 200 (empty) */ QueryHistoryDeleteQueryResponse; +export type DeleteQueryApiArg = { + queryHistoryUid: string; +}; +export type PatchQueryCommentApiResponse = /** status 200 (empty) */ QueryHistoryResponse; +export type PatchQueryCommentApiArg = { + queryHistoryUid: string; + patchQueryCommentInQueryHistoryCommand: PatchQueryCommentInQueryHistoryCommand; +}; +export type ListRecordingRulesApiResponse = /** status 200 (empty) */ RecordingRuleJson[]; +export type ListRecordingRulesApiArg = void; +export type CreateRecordingRuleApiResponse = /** status 200 (empty) */ RecordingRuleJson; +export type CreateRecordingRuleApiArg = { + recordingRuleJson: RecordingRuleJson; +}; +export type UpdateRecordingRuleApiResponse = /** status 200 (empty) */ RecordingRuleJson; +export type UpdateRecordingRuleApiArg = { + recordingRuleJson: RecordingRuleJson; +}; +export type TestCreateRecordingRuleApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type TestCreateRecordingRuleApiArg = { + recordingRuleJson: RecordingRuleJson; +}; +export type DeleteRecordingRuleWriteTargetApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteRecordingRuleWriteTargetApiArg = void; +export type GetRecordingRuleWriteTargetApiResponse = /** status 200 (empty) */ PrometheusRemoteWriteTargetJson; +export type GetRecordingRuleWriteTargetApiArg = void; +export type CreateRecordingRuleWriteTargetApiResponse = /** status 200 (empty) */ PrometheusRemoteWriteTargetJson; +export type CreateRecordingRuleWriteTargetApiArg = { + prometheusRemoteWriteTargetJson: PrometheusRemoteWriteTargetJson; +}; +export type DeleteRecordingRuleApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteRecordingRuleApiArg = { + recordingRuleId: number; +}; +export type GetReportsApiResponse = /** status 200 (empty) */ Report[]; +export type GetReportsApiArg = void; +export type CreateReportApiResponse = /** status 200 (empty) */ { + id?: number; + message?: string; +}; +export type CreateReportApiArg = { + createOrUpdateReport: CreateOrUpdateReport; +}; +export type GetReportsByDashboardUidApiResponse = /** status 200 (empty) */ Report[]; +export type GetReportsByDashboardUidApiArg = { + uid: string; +}; +export type SendReportApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SendReportApiArg = { + reportEmail: ReportEmail; +}; +export type GetSettingsImageApiResponse = /** status 200 (empty) */ number[]; +export type GetSettingsImageApiArg = void; +export type RenderReportCsVsApiResponse = /** status 200 (empty) */ number[] | /** status 204 (empty) */ object; +export type RenderReportCsVsApiArg = { + dashboards?: string; + title?: string; +}; +export type RenderReportPdFsApiResponse = /** status 200 (empty) */ number[]; +export type RenderReportPdFsApiArg = { + dashboards?: string; + orientation?: string; + layout?: string; + title?: string; + scaleFactor?: string; + includeTables?: string; +}; +export type GetReportSettingsApiResponse = /** status 200 (empty) */ ReportSettings; +export type GetReportSettingsApiArg = void; +export type SaveReportSettingsApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SaveReportSettingsApiArg = { + reportSettings: ReportSettings; +}; +export type SendTestEmailApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SendTestEmailApiArg = { + createOrUpdateReport: CreateOrUpdateReport; +}; +export type PostAcsApiResponse = unknown; +export type PostAcsApiArg = { + relayState?: string; +}; +export type GetMetadataApiResponse = /** status 200 (empty) */ number[]; +export type GetMetadataApiArg = void; +export type GetSloApiResponse = unknown; +export type GetSloApiArg = void; +export type PostSloApiResponse = unknown; +export type PostSloApiArg = { + samlRequest?: string; + samlResponse?: string; +}; +export type SearchApiResponse = /** status 200 (empty) */ HitList; +export type SearchApiArg = { + /** Search Query */ + query?: string; + /** List of tags to search for */ + tag?: string[]; + /** Type to search for, dash-folder or dash-db */ + type?: 'dash-folder' | 'dash-db'; + /** List of dashboard id’s to search for + This is deprecated: users should use the `dashboardUIDs` query parameter instead */ + dashboardIds?: number[]; + /** List of dashboard uid’s to search for */ + dashboardUiDs?: string[]; + /** List of folder id’s to search in for dashboards + If it's `0` then it will query for the top level folders + This is deprecated: users should use the `folderUIDs` query parameter instead */ + folderIds?: number[]; + /** List of folder UID’s to search in for dashboards + If it's an empty string then it will query for the top level folders */ + folderUiDs?: string[]; + /** Flag indicating if only starred Dashboards should be returned */ + starred?: boolean; + /** Limit the number of returned results (max 5000) */ + limit?: number; + /** Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size. Only available in Grafana v6.2+. */ + page?: number; + /** Set to `Edit` to return dashboards/folders that the user can edit */ + permission?: 'Edit' | 'View'; + /** Sort method; for listing all the possible sort methods use the search sorting endpoint. */ + sort?: 'alpha-asc' | 'alpha-desc'; + /** Flag indicating if only soft deleted Dashboards should be returned */ + deleted?: boolean; +}; +export type ListSortOptionsApiResponse = /** status 200 (empty) */ { + description?: string; + displayName?: string; + meta?: string; + name?: string; +}; +export type ListSortOptionsApiArg = void; +export type CreateServiceAccountApiResponse = /** status 201 (empty) */ ServiceAccountDto; +export type CreateServiceAccountApiArg = { + createServiceAccountForm: CreateServiceAccountForm; +}; +export type SearchOrgServiceAccountsWithPagingApiResponse = /** status 200 (empty) */ SearchOrgServiceAccountsResult; +export type SearchOrgServiceAccountsWithPagingApiArg = { + disabled?: boolean; + expiredTokens?: boolean; + /** It will return results where the query value is contained in one of the name. + Query values with spaces need to be URL encoded. */ + query?: string; + /** The default value is 1000. */ + perpage?: number; + /** The default value is 1. */ + page?: number; +}; +export type DeleteServiceAccountApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteServiceAccountApiArg = { + serviceAccountId: number; +}; +export type RetrieveServiceAccountApiResponse = /** status 200 (empty) */ ServiceAccountDto; +export type RetrieveServiceAccountApiArg = { + serviceAccountId: number; +}; +export type UpdateServiceAccountApiResponse = /** status 200 (empty) */ { + id?: number; + message?: string; + name?: string; + serviceaccount?: ServiceAccountProfileDto; +}; +export type UpdateServiceAccountApiArg = { + serviceAccountId: number; + updateServiceAccountForm: UpdateServiceAccountForm; +}; +export type ListTokensApiResponse = /** status 200 (empty) */ TokenDto[]; +export type ListTokensApiArg = { + serviceAccountId: number; +}; +export type CreateTokenApiResponse = /** status 200 (empty) */ NewApiKeyResult; +export type CreateTokenApiArg = { + serviceAccountId: number; + addServiceAccountTokenCommand: AddServiceAccountTokenCommand; +}; +export type DeleteTokenApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteTokenApiArg = { + tokenId: number; + serviceAccountId: number; +}; +export type RetrieveJwksApiResponse = /** status 200 (empty) */ { + keys?: JsonWebKey[]; +}; +export type RetrieveJwksApiArg = void; +export type GetSharingOptionsApiResponse = /** status 200 (empty) */ { + externalEnabled?: boolean; + externalSnapshotName?: string; + externalSnapshotURL?: string; +}; +export type GetSharingOptionsApiArg = void; +export type CreateDashboardSnapshotApiResponse = /** status 200 (empty) */ { + /** Unique key used to delete the snapshot. It is different from the key so that only the creator can delete the snapshot. */ + deleteKey?: string; + deleteUrl?: string; + /** Snapshot id */ + id?: number; + /** Unique key */ + key?: string; + url?: string; +}; +export type CreateDashboardSnapshotApiArg = { + createDashboardSnapshotCommand: CreateDashboardSnapshotCommand; +}; +export type DeleteDashboardSnapshotByDeleteKeyApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteDashboardSnapshotByDeleteKeyApiArg = { + deleteKey: string; +}; +export type DeleteDashboardSnapshotApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteDashboardSnapshotApiArg = { + key: string; +}; +export type GetDashboardSnapshotApiResponse = unknown; +export type GetDashboardSnapshotApiArg = { + key: string; +}; +export type CreateTeamApiResponse = /** status 200 (empty) */ { + message?: string; + teamId?: number; + uid?: string; +}; +export type CreateTeamApiArg = { + createTeamCommand: CreateTeamCommand; +}; +export type SearchTeamsApiResponse = /** status 200 (empty) */ SearchTeamQueryResult; +export type SearchTeamsApiArg = { + page?: number; + /** Number of items per page + The totalCount field in the response can be used for pagination list E.g. if totalCount is equal to 100 teams and the perpage parameter is set to 10 then there are 10 pages of teams. */ + perpage?: number; + name?: string; + /** If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded. */ + query?: string; + accesscontrol?: boolean; + sort?: string; +}; +export type RemoveTeamGroupApiQueryApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RemoveTeamGroupApiQueryApiArg = { + groupId?: string; + teamId: number; +}; +export type GetTeamGroupsApiApiResponse = /** status 200 (empty) */ TeamGroupDto[]; +export type GetTeamGroupsApiApiArg = { + teamId: number; +}; +export type AddTeamGroupApiApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AddTeamGroupApiApiArg = { + teamId: number; + teamGroupMapping: TeamGroupMapping; +}; +export type SearchTeamGroupsApiResponse = /** status 200 (empty) */ SearchTeamGroupsQueryResult; +export type SearchTeamGroupsApiArg = { + teamId: number; + page?: number; + /** Number of items per page */ + perpage?: number; + /** If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded. */ + query?: string; + /** Filter by exact name match */ + name?: string; +}; +export type DeleteTeamByIdApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type DeleteTeamByIdApiArg = { + teamId: string; +}; +export type GetTeamByIdApiResponse = /** status 200 (empty) */ TeamDto; +export type GetTeamByIdApiArg = { + teamId: string; +}; +export type UpdateTeamApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateTeamApiArg = { + teamId: string; + updateTeamCommand: UpdateTeamCommand; +}; +export type GetTeamMembersApiResponse = /** status 200 (empty) */ TeamMemberDto[]; +export type GetTeamMembersApiArg = { + teamId: string; +}; +export type AddTeamMemberApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type AddTeamMemberApiArg = { + teamId: string; + addTeamMemberCommand: AddTeamMemberCommand; +}; +export type SetTeamMembershipsApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type SetTeamMembershipsApiArg = { + teamId: string; + setTeamMembershipsCommand: SetTeamMembershipsCommand; +}; +export type RemoveTeamMemberApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RemoveTeamMemberApiArg = { + teamId: string; + userId: number; +}; +export type UpdateTeamMemberApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateTeamMemberApiArg = { + teamId: string; + userId: number; + updateTeamMemberCommand: UpdateTeamMemberCommand; +}; +export type GetTeamPreferencesApiResponse = /** status 200 (empty) */ PreferencesSpec; +export type GetTeamPreferencesApiArg = { + teamId: string; +}; +export type UpdateTeamPreferencesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateTeamPreferencesApiArg = { + teamId: string; + updatePrefsCmd: UpdatePrefsCmd; +}; +export type GetSignedInUserApiResponse = /** status 200 (empty) */ UserProfileDto; +export type GetSignedInUserApiArg = void; +export type UpdateSignedInUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateSignedInUserApiArg = { + /** To change the email, name, login, theme, provide another one. */ + updateUserCommand: UpdateUserCommand; +}; +export type GetUserAuthTokensApiResponse = /** status 200 (empty) */ UserToken[]; +export type GetUserAuthTokensApiArg = void; +export type UpdateUserEmailApiResponse = unknown; +export type UpdateUserEmailApiArg = void; +export type ClearHelpFlagsApiResponse = /** status 200 (empty) */ { + helpFlags1?: number; + message?: string; +}; +export type ClearHelpFlagsApiArg = void; +export type SetHelpFlagApiResponse = /** status 200 (empty) */ { + helpFlags1?: number; + message?: string; +}; +export type SetHelpFlagApiArg = { + flagId: string; +}; +export type GetSignedInUserOrgListApiResponse = /** status 200 (empty) */ UserOrgDto[]; +export type GetSignedInUserOrgListApiArg = void; +export type ChangeUserPasswordApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type ChangeUserPasswordApiArg = { + /** To change the email, name, login, theme, provide another one. */ + changeUserPasswordCommand: ChangeUserPasswordCommand; +}; +export type GetUserPreferencesApiResponse = /** status 200 (empty) */ PreferencesSpec; +export type GetUserPreferencesApiArg = void; +export type PatchUserPreferencesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type PatchUserPreferencesApiArg = { + patchPrefsCmd: PatchPrefsCmd; +}; +export type UpdateUserPreferencesApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateUserPreferencesApiArg = { + updatePrefsCmd: UpdatePrefsCmd; +}; +export type GetUserQuotasApiResponse = /** status 200 (empty) */ QuotaDto[]; +export type GetUserQuotasApiArg = void; +export type RevokeUserAuthTokenApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RevokeUserAuthTokenApiArg = { + revokeAuthTokenCmd: RevokeAuthTokenCmd; +}; +export type UnstarDashboardByUidApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UnstarDashboardByUidApiArg = { + dashboardUid: string; +}; +export type StarDashboardByUidApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type StarDashboardByUidApiArg = { + dashboardUid: string; +}; +export type GetSignedInUserTeamListApiResponse = /** status 200 (empty) */ TeamDto[]; +export type GetSignedInUserTeamListApiArg = void; +export type UserSetUsingOrgApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UserSetUsingOrgApiArg = { + orgId: number; +}; +export type SearchUsersApiResponse = /** status 200 (empty) */ UserSearchHitDto[]; +export type SearchUsersApiArg = { + /** Limit the maximum number of users to return per page */ + perpage?: number; + /** Page index for starting fetching users */ + page?: number; +}; +export type GetUserByLoginOrEmailApiResponse = /** status 200 (empty) */ UserProfileDto; +export type GetUserByLoginOrEmailApiArg = { + /** loginOrEmail of the user */ + loginOrEmail: string; +}; +export type SearchUsersWithPagingApiResponse = /** status 200 (empty) */ SearchUserQueryResult; +export type SearchUsersWithPagingApiArg = void; +export type GetUserByIdApiResponse = /** status 200 (empty) */ UserProfileDto; +export type GetUserByIdApiArg = { + userId: number; +}; +export type UpdateUserApiResponse = + /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateUserApiArg = { + userId: number; + /** To change the email, name, login, theme, provide another one. */ + updateUserCommand: UpdateUserCommand; +}; +export type GetUserOrgListApiResponse = /** status 200 (empty) */ UserOrgDto[]; +export type GetUserOrgListApiArg = { + userId: number; +}; +export type GetUserTeamsApiResponse = /** status 200 (empty) */ TeamDto[]; +export type GetUserTeamsApiArg = { + userId: number; +}; +export type RouteGetAlertRulesApiResponse = /** status 200 ProvisionedAlertRules */ ProvisionedAlertRulesRead; +export type RouteGetAlertRulesApiArg = void; +export type RoutePostAlertRuleApiResponse = /** status 201 ProvisionedAlertRule */ ProvisionedAlertRuleRead; +export type RoutePostAlertRuleApiArg = { + 'X-Disable-Provenance'?: string; + provisionedAlertRule: ProvisionedAlertRule; +}; +export type RouteGetAlertRulesExportApiResponse = + /** status 200 AlertingFileExport */ AlertingFileExportIsTheFullProvisionedFileExport; +export type RouteGetAlertRulesExportApiArg = { + /** Whether to initiate a download of the file or not. */ + download?: boolean; + /** Format of the downloaded file. Supported yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. */ + format?: 'yaml' | 'json' | 'hcl'; + /** UIDs of folders from which to export rules */ + folderUid?: string[]; + /** Name of group of rules to export. Must be specified only together with a single folder UID */ + group?: string; + /** UID of alert rule to export. If specified, parameters folderUid and group must be empty. */ + ruleUid?: string; +}; +export type RouteDeleteAlertRuleApiResponse = unknown; +export type RouteDeleteAlertRuleApiArg = { + /** Alert rule UID */ + uid: string; + 'X-Disable-Provenance'?: string; +}; +export type RouteGetAlertRuleApiResponse = /** status 200 ProvisionedAlertRule */ ProvisionedAlertRuleRead; +export type RouteGetAlertRuleApiArg = { + /** Alert rule UID */ + uid: string; +}; +export type RoutePutAlertRuleApiResponse = /** status 200 ProvisionedAlertRule */ ProvisionedAlertRuleRead; +export type RoutePutAlertRuleApiArg = { + /** Alert rule UID */ + uid: string; + 'X-Disable-Provenance'?: string; + provisionedAlertRule: ProvisionedAlertRule; +}; +export type RouteGetAlertRuleExportApiResponse = + /** status 200 AlertingFileExport */ AlertingFileExportIsTheFullProvisionedFileExport; +export type RouteGetAlertRuleExportApiArg = { + /** Whether to initiate a download of the file or not. */ + download?: boolean; + /** Format of the downloaded file. Supported yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. */ + format?: 'yaml' | 'json' | 'hcl'; + /** Alert rule UID */ + uid: string; +}; +export type RouteGetContactpointsApiResponse = /** status 200 ContactPoints */ ContactPointsRead; +export type RouteGetContactpointsApiArg = { + /** Filter by name */ + name?: string; +}; +export type RoutePostContactpointsApiResponse = /** status 202 EmbeddedContactPoint */ EmbeddedContactPointRead; +export type RoutePostContactpointsApiArg = { + 'X-Disable-Provenance'?: string; + embeddedContactPoint: EmbeddedContactPoint; +}; +export type RouteGetContactpointsExportApiResponse = + /** status 200 AlertingFileExport */ AlertingFileExportIsTheFullProvisionedFileExport; +export type RouteGetContactpointsExportApiArg = { + /** Whether to initiate a download of the file or not. */ + download?: boolean; + /** Format of the downloaded file. Supported yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. */ + format?: 'yaml' | 'json' | 'hcl'; + /** Whether any contained secure settings should be decrypted or left redacted. Redacted settings will contain RedactedValue instead. Currently, only org admin can view decrypted secure settings. */ + decrypt?: boolean; + /** Filter by name */ + name?: string; +}; +export type RouteDeleteContactpointsApiResponse = unknown; +export type RouteDeleteContactpointsApiArg = { + /** UID is the contact point unique identifier */ + uid: string; +}; +export type RoutePutContactpointApiResponse = /** status 202 Ack */ Ack; +export type RoutePutContactpointApiArg = { + /** UID is the contact point unique identifier */ + uid: string; + 'X-Disable-Provenance'?: string; + embeddedContactPoint: EmbeddedContactPoint; +}; +export type RouteDeleteAlertRuleGroupApiResponse = unknown; +export type RouteDeleteAlertRuleGroupApiArg = { + folderUid: string; + group: string; +}; +export type RouteGetAlertRuleGroupApiResponse = /** status 200 AlertRuleGroup */ AlertRuleGroupRead; +export type RouteGetAlertRuleGroupApiArg = { + folderUid: string; + group: string; +}; +export type RoutePutAlertRuleGroupApiResponse = /** status 200 AlertRuleGroup */ AlertRuleGroupRead; +export type RoutePutAlertRuleGroupApiArg = { + 'X-Disable-Provenance'?: string; + folderUid: string; + group: string; + alertRuleGroup: AlertRuleGroup; +}; +export type RouteGetAlertRuleGroupExportApiResponse = + /** status 200 AlertingFileExport */ AlertingFileExportIsTheFullProvisionedFileExport; +export type RouteGetAlertRuleGroupExportApiArg = { + /** Whether to initiate a download of the file or not. */ + download?: boolean; + /** Format of the downloaded file. Supported yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. */ + format?: 'yaml' | 'json' | 'hcl'; + folderUid: string; + group: string; +}; +export type RouteGetMuteTimingsApiResponse = /** status 200 MuteTimings */ MuteTimings; +export type RouteGetMuteTimingsApiArg = void; +export type RoutePostMuteTimingApiResponse = + /** status 201 MuteTimeInterval */ MuteTimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted; +export type RoutePostMuteTimingApiArg = { + 'X-Disable-Provenance'?: string; + muteTimeInterval: MuteTimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted; +}; +export type RouteExportMuteTimingsApiResponse = + /** status 200 AlertingFileExport */ AlertingFileExportIsTheFullProvisionedFileExport; +export type RouteExportMuteTimingsApiArg = { + /** Whether to initiate a download of the file or not. */ + download?: boolean; + /** Format of the downloaded file. Supported yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. */ + format?: 'yaml' | 'json' | 'hcl'; +}; +export type RouteDeleteMuteTimingApiResponse = unknown; +export type RouteDeleteMuteTimingApiArg = { + /** Mute timing name */ + name: string; + /** Version of mute timing to use for optimistic concurrency. Leave empty to disable validation */ + version?: string; + 'X-Disable-Provenance'?: string; +}; +export type RouteGetMuteTimingApiResponse = + /** status 200 MuteTimeInterval */ MuteTimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted; +export type RouteGetMuteTimingApiArg = { + /** Mute timing name */ + name: string; +}; +export type RoutePutMuteTimingApiResponse = + /** status 202 MuteTimeInterval */ MuteTimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted; +export type RoutePutMuteTimingApiArg = { + /** Mute timing name */ + name: string; + 'X-Disable-Provenance'?: string; + muteTimeInterval: MuteTimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted; +}; +export type RouteExportMuteTimingApiResponse = + /** status 200 AlertingFileExport */ AlertingFileExportIsTheFullProvisionedFileExport; +export type RouteExportMuteTimingApiArg = { + /** Whether to initiate a download of the file or not. */ + download?: boolean; + /** Format of the downloaded file. Supported yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. */ + format?: 'yaml' | 'json' | 'hcl'; + /** Mute timing name */ + name: string; +}; +export type RouteResetPolicyTreeApiResponse = /** status 202 Ack */ Ack; +export type RouteResetPolicyTreeApiArg = void; +export type RouteGetPolicyTreeApiResponse = /** status 200 Route */ Route; +export type RouteGetPolicyTreeApiArg = void; +export type RoutePutPolicyTreeApiResponse = /** status 202 Ack */ Ack; +export type RoutePutPolicyTreeApiArg = { + 'X-Disable-Provenance'?: string; + /** The new notification routing tree to use */ + route: Route; +}; +export type RouteGetPolicyTreeExportApiResponse = + /** status 200 AlertingFileExport */ AlertingFileExportIsTheFullProvisionedFileExport; +export type RouteGetPolicyTreeExportApiArg = void; +export type RouteGetTemplatesApiResponse = /** status 200 NotificationTemplates */ NotificationTemplates; +export type RouteGetTemplatesApiArg = void; +export type RouteDeleteTemplateApiResponse = unknown; +export type RouteDeleteTemplateApiArg = { + /** Template group name */ + name: string; + /** Version of template to use for optimistic concurrency. Leave empty to disable validation */ + version?: string; +}; +export type RouteGetTemplateApiResponse = /** status 200 NotificationTemplate */ NotificationTemplate; +export type RouteGetTemplateApiArg = { + /** Template group name */ + name: string; +}; +export type RoutePutTemplateApiResponse = /** status 202 NotificationTemplate */ NotificationTemplate; +export type RoutePutTemplateApiArg = { + /** Template group name */ + name: string; + 'X-Disable-Provenance'?: string; + notificationTemplateContent: NotificationTemplateContent; +}; +export type ListAllProvidersSettingsApiResponse = /** status 200 (empty) */ { + id?: string; + provider?: string; + settings?: { + [key: string]: any; + }; + source?: string; +}[]; +export type ListAllProvidersSettingsApiArg = void; +export type RemoveProviderSettingsApiResponse = + /** status 204 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type RemoveProviderSettingsApiArg = { + key: string; +}; +export type GetProviderSettingsApiResponse = /** status 200 (empty) */ { + id?: string; + provider?: string; + settings?: { + [key: string]: any; + }; + source?: string; +}; +export type GetProviderSettingsApiArg = { + key: string; +}; +export type UpdateProviderSettingsApiResponse = + /** status 204 An OKResponse is returned if the request was successful. */ SuccessResponseBody; +export type UpdateProviderSettingsApiArg = { + key: string; + body: { + id?: string; + provider?: string; + settings?: { + [key: string]: any; + }; + }; +}; +export type SearchResultItem = { + action?: string; + basicRole?: string; + orgId?: number; + roleName?: string; + scope?: string; + teamId?: number; + userId?: number; + version?: number; +}; +export type SearchResult = { + result?: SearchResultItem[]; +}; +export type ErrorResponseBody = { + /** Error An optional detailed description of the actual error. Only included if running in developer mode. */ + error?: string; + /** a human readable version of the error */ + message: string; + /** Status An optional status to denote the cause of the error. + + For example, a 412 Precondition Failed error may include additional information of why that error happened. */ + status?: string; +}; +export type Permission = { + action?: string; + created?: string; + scope?: string; + updated?: string; +}; +export type RoleDto = { + created: string; + delegatable?: boolean; + description: string; + displayName: string; + global?: boolean; + group: string; + hidden?: boolean; + mapped?: boolean; + name: string; + permissions?: Permission[]; + uid: string; + updated: string; + version: number; +}; +export type CreateRoleForm = { + description?: string; + displayName?: string; + global?: boolean; + group?: string; + hidden?: boolean; + name?: string; + permissions?: Permission[]; + uid?: string; + version?: number; +}; +export type SuccessResponseBody = { + message?: string; +}; +export type UpdateRoleCommand = { + description: string; + displayName: string; + global?: boolean; + group: string; + hidden?: boolean; + name?: string; + permissions?: Permission[]; + version?: number; +}; +export type RoleAssignmentsDto = { + role_uid?: string; + service_accounts?: number[]; + teams?: number[]; + users?: number[]; +}; +export type SetRoleAssignmentsCommand = { + service_accounts?: number[]; + teams?: number[]; + users?: number[]; +}; +export type Status = number; +export type RolesSearchQuery = { + includeHidden?: boolean; + orgId?: number; + teamIds?: number[]; + userIds?: number[]; +}; +export type AddTeamRoleCommand = { + roleUid?: string; +}; +export type SetTeamRolesCommand = { + includeHidden?: boolean; + roleUids?: string[]; +}; +export type AddUserRoleCommand = { + global?: boolean; + roleUid?: string; +}; +export type SetUserRolesCommand = { + global?: boolean; + includeHidden?: boolean; + roleUids?: string[]; +}; +export type Assignments = { + builtInRoles?: boolean; + serviceAccounts?: boolean; + teams?: boolean; + users?: boolean; +}; +export type Description = { + assignments?: Assignments; + permissions?: string[]; +}; +export type ResourcePermissionDto = { + actions?: string[]; + builtInRole?: string; + id?: number; + isInherited?: boolean; + isManaged?: boolean; + isServiceAccount?: boolean; + permission?: string; + roleName?: string; + team?: string; + teamAvatarUrl?: string; + teamId?: number; + teamUid?: string; + userAvatarUrl?: string; + userId?: number; + userLogin?: string; + userUid?: string; +}; +export type SetResourcePermissionCommand = { + builtInRole?: string; + permission?: string; + teamId?: number; + userId?: number; +}; +export type SetPermissionsCommand = { + permissions?: SetResourcePermissionCommand[]; +}; +export type SetPermissionCommand = { + permission?: string; +}; +export type Duration = number; +export type FailedUser = { + Error?: string; + Login?: string; +}; +export type SyncResultHoldsTheResultOfASyncWithLdapThisGivesUsInformationOnWhichUsersWereUpdatedAndHow = { + Elapsed?: Duration; + FailedUsers?: FailedUser[]; + MissingUserIds?: number[]; + Started?: string; + UpdatedUserIds?: number[]; +}; +export type ActiveSyncStatusDto = { + enabled?: boolean; + nextSync?: string; + prevSync?: SyncResultHoldsTheResultOfASyncWithLdapThisGivesUsInformationOnWhichUsersWereUpdatedAndHow; + schedule?: string; +}; +export type SettingsBag = { + [key: string]: { + [key: string]: string; + }; +}; +export type AdminStats = { + activeAdmins?: number; + activeDevices?: number; + activeEditors?: number; + activeSessions?: number; + activeUsers?: number; + activeViewers?: number; + admins?: number; + alerts?: number; + dailyActiveAdmins?: number; + dailyActiveEditors?: number; + dailyActiveSessions?: number; + dailyActiveUsers?: number; + dailyActiveViewers?: number; + dashboards?: number; + datasources?: number; + editors?: number; + monthlyActiveUsers?: number; + orgs?: number; + playlists?: number; + snapshots?: number; + stars?: number; + tags?: number; + users?: number; + viewers?: number; +}; +export type AdminCreateUserResponse = { + id?: number; + message?: string; + uid?: string; +}; +export type Password = string; +export type AdminCreateUserForm = { + email?: string; + login?: string; + name?: string; + orgId?: number; + password?: Password; +}; +export type UserToken = { + AuthToken?: string; + AuthTokenSeen?: boolean; + ClientIp?: string; + CreatedAt?: number; + ExternalSessionId?: number; + Id?: number; + PrevAuthToken?: string; + RevokedAt?: number; + RotatedAt?: number; + SeenAt?: number; + UnhashedToken?: string; + UpdatedAt?: number; + UserAgent?: string; + UserId?: number; +}; +export type AdminUpdateUserPasswordForm = { + password?: Password; +}; +export type AdminUpdateUserPermissionsForm = { + isGrafanaAdmin?: boolean; +}; +export type QuotaDto = { + limit?: number; + org_id?: number; + target?: string; + used?: number; + user_id?: number; +}; +export type UpdateQuotaCmd = { + limit?: number; + target?: string; +}; +export type RevokeAuthTokenCmd = { + authTokenId?: number; +}; +export type Json = object; +export type Annotation = { + alertId?: number; + alertName?: string; + avatarUrl?: string; + created?: number; + /** Deprecated: Use DashboardUID and OrgID instead */ + dashboardId?: number; + dashboardUID?: string; + data?: Json; + email?: string; + id?: number; + login?: string; + newState?: string; + panelId?: number; + prevState?: string; + tags?: string[]; + text?: string; + time?: number; + timeEnd?: number; + updated?: number; + userId?: number; +}; +export type PostAnnotationsCmd = { + dashboardId?: number; + dashboardUID?: string; + data?: Json; + panelId?: number; + tags?: string[]; + text: string; + time?: number; + timeEnd?: number; +}; +export type PostGraphiteAnnotationsCmd = { + data?: string; + tags?: any; + what?: string; + when?: number; +}; +export type MassDeleteAnnotationsCmd = { + annotationId?: number; + dashboardId?: number; + dashboardUID?: string; + panelId?: number; +}; +export type TagsDtoIsTheFrontendDtoForTag = { + count?: number; + tag?: string; +}; +export type FindTagsResultIsTheResultOfATagsSearch = { + tags?: TagsDtoIsTheFrontendDtoForTag[]; +}; +export type GetAnnotationTagsResponseIsAResponseStructForFindTagsResult = { + result?: FindTagsResultIsTheResultOfATagsSearch; +}; +export type PatchAnnotationsCmd = { + data?: Json; + id?: number; + tags?: string[]; + text?: string; + time?: number; + timeEnd?: number; +}; +export type UpdateAnnotationsCmd = { + data?: Json; + id?: number; + tags?: string[]; + text?: string; + time?: number; + timeEnd?: number; +}; +export type DeviceDto = { + avatarUrl?: string; + clientIp?: string; + createdAt?: string; + deviceId?: string; + lastSeenAt?: string; + updatedAt?: string; + userAgent?: string; +}; +export type DeviceSearchHitDto = { + clientIp?: string; + createdAt?: string; + deviceId?: string; + lastSeenAt?: string; + updatedAt?: string; + userAgent?: string; +}; +export type SearchDeviceQueryResult = { + devices?: DeviceSearchHitDto[]; + page?: number; + perPage?: number; + totalCount?: number; +}; +export type CloudMigrationSessionResponseDto = { + created?: string; + slug?: string; + uid?: string; + updated?: string; +}; +export type CloudMigrationSessionListResponseDto = { + sessions?: CloudMigrationSessionResponseDto[]; +}; +export type CloudMigrationSessionRequestDto = { + authToken?: string; +}; +export type CreateSnapshotResponseDto = { + uid?: string; +}; +export type CreateSnapshotRequestDto = { + resourceTypes?: ( + | 'DASHBOARD' + | 'DATASOURCE' + | 'FOLDER' + | 'LIBRARY_ELEMENT' + | 'ALERT_RULE' + | 'ALERT_RULE_GROUP' + | 'CONTACT_POINT' + | 'NOTIFICATION_POLICY' + | 'NOTIFICATION_TEMPLATE' + | 'MUTE_TIMING' + | 'PLUGIN' + )[]; +}; +export type MigrateDataResponseItemDto = { + errorCode?: + | 'ALERT_RULES_QUOTA_REACHED' + | 'ALERT_RULES_GROUP_QUOTA_REACHED' + | 'DATASOURCE_NAME_CONFLICT' + | 'DATASOURCE_INVALID_URL' + | 'DATASOURCE_ALREADY_MANAGED' + | 'FOLDER_NAME_CONFLICT' + | 'DASHBOARD_ALREADY_MANAGED' + | 'LIBRARY_ELEMENT_NAME_CONFLICT' + | 'UNSUPPORTED_DATA_TYPE' + | 'RESOURCE_CONFLICT' + | 'UNEXPECTED_STATUS_CODE' + | 'INTERNAL_SERVICE_ERROR' + | 'GENERIC_ERROR'; + message?: string; + name?: string; + parentName?: string; + refId: string; + status: 'OK' | 'WARNING' | 'ERROR' | 'PENDING' | 'UNKNOWN'; + type: + | 'DASHBOARD' + | 'DATASOURCE' + | 'FOLDER' + | 'LIBRARY_ELEMENT' + | 'ALERT_RULE' + | 'ALERT_RULE_GROUP' + | 'CONTACT_POINT' + | 'NOTIFICATION_POLICY' + | 'NOTIFICATION_TEMPLATE' + | 'MUTE_TIMING' + | 'PLUGIN'; +}; +export type SnapshotResourceStats = { + statuses?: { + [key: string]: number; + }; + total?: number; + types?: { + [key: string]: number; + }; +}; +export type GetSnapshotResponseDto = { + created?: string; + finished?: string; + results?: MigrateDataResponseItemDto[]; + sessionUid?: string; + stats?: SnapshotResourceStats; + status?: + | 'INITIALIZING' + | 'CREATING' + | 'PENDING_UPLOAD' + | 'UPLOADING' + | 'PENDING_PROCESSING' + | 'PROCESSING' + | 'FINISHED' + | 'CANCELED' + | 'ERROR' + | 'UNKNOWN'; + uid?: string; +}; +export type SnapshotDto = { + created?: string; + finished?: string; + sessionUid?: string; + status?: + | 'INITIALIZING' + | 'CREATING' + | 'PENDING_UPLOAD' + | 'UPLOADING' + | 'PENDING_PROCESSING' + | 'PROCESSING' + | 'FINISHED' + | 'CANCELED' + | 'ERROR' + | 'UNKNOWN'; + uid?: string; +}; +export type SnapshotListResponseDto = { + snapshots?: SnapshotDto[]; +}; +export type ResourceDependencyDto = { + dependencies?: ( + | 'DASHBOARD' + | 'DATASOURCE' + | 'FOLDER' + | 'LIBRARY_ELEMENT' + | 'ALERT_RULE' + | 'ALERT_RULE_GROUP' + | 'CONTACT_POINT' + | 'NOTIFICATION_POLICY' + | 'NOTIFICATION_TEMPLATE' + | 'MUTE_TIMING' + | 'PLUGIN' + )[]; + resourceType?: + | 'DASHBOARD' + | 'DATASOURCE' + | 'FOLDER' + | 'LIBRARY_ELEMENT' + | 'ALERT_RULE' + | 'ALERT_RULE_GROUP' + | 'CONTACT_POINT' + | 'NOTIFICATION_POLICY' + | 'NOTIFICATION_TEMPLATE' + | 'MUTE_TIMING' + | 'PLUGIN'; +}; +export type ResourceDependenciesResponseDto = { + resourceDependencies?: ResourceDependencyDto[]; +}; +export type GetAccessTokenResponseDto = { + createdAt?: string; + displayName?: string; + expiresAt?: string; + firstUsedAt?: string; + id?: string; + lastUsedAt?: string; +}; +export type CreateAccessTokenResponseDto = { + token?: string; +}; +export type ConvertPrometheusResponse = { + error?: string; + errorType?: string; + status?: string; +}; +export type PublicError = { + extra?: { + [key: string]: any; + }; + message?: string; + messageId?: string; + statusCode?: number; +}; +export type ForbiddenError = { + body?: PublicError; +}; +export type PrometheusRule = { + alert?: string; + annotations?: { + [key: string]: string; + }; + expr?: string; + for?: string; + keep_firing_for?: string; + labels?: { + [key: string]: string; + }; + record?: string; +}; +export type PrometheusRuleGroup = { + interval?: Duration; + labels?: { + [key: string]: string; + }; + limit?: number; + name?: string; + query_offset?: string; + rules?: PrometheusRule[]; +}; +export type DashboardSnapshotDto = { + created?: string; + expires?: string; + external?: boolean; + externalUrl?: string; + key?: string; + name?: string; + updated?: string; +}; +export type CalculateDiffTarget = { + dashboardId?: number; + unsavedDashboard?: Json; + version?: number; +}; +export type SaveDashboardCommand = { + UpdatedAt?: string; + dashboard?: Json; + /** Deprecated: use FolderUID instead */ + folderId?: number; + folderUid?: string; + isFolder?: boolean; + message?: string; + overwrite?: boolean; + userId?: number; +}; +export type AnnotationActions = { + canAdd?: boolean; + canDelete?: boolean; + canEdit?: boolean; +}; +export type AnnotationPermission = { + dashboard?: AnnotationActions; + organization?: AnnotationActions; +}; +export type DashboardMeta = { + annotationsPermissions?: AnnotationPermission; + apiVersion?: string; + canAdmin?: boolean; + canDelete?: boolean; + canEdit?: boolean; + canSave?: boolean; + canStar?: boolean; + created?: string; + createdBy?: string; + expires?: string; + /** Deprecated: use FolderUID instead */ + folderId?: number; + folderTitle?: string; + folderUid?: string; + folderUrl?: string; + hasAcl?: boolean; + isFolder?: boolean; + isSnapshot?: boolean; + isStarred?: boolean; + provisioned?: boolean; + provisionedExternalId?: string; + publicDashboardEnabled?: boolean; + slug?: string; + type?: string; + updated?: string; + updatedBy?: string; + url?: string; + version?: number; +}; +export type GetHomeDashboardResponse = { + dashboard?: Json; + meta?: DashboardMeta; +} & { + redirectUri?: string; +}; +export type ImportDashboardResponseResponseObjectReturnedWhenImportingADashboard = { + dashboardId?: number; + description?: string; + /** Deprecated: use FolderUID instead */ + folderId?: number; + folderUid?: string; + imported?: boolean; + importedRevision?: number; + importedUri?: string; + importedUrl?: string; + path?: string; + pluginId?: string; + removed?: boolean; + revision?: number; + slug?: string; + title?: string; + uid?: string; +}; +export type ImportDashboardInputDefinitionOfInputParametersWhenImportingADashboard = { + name?: string; + pluginId?: string; + type?: string; + value?: string; +}; +export type ImportDashboardRequestRequestObjectForImportingADashboard = { + dashboard?: Json; + /** Deprecated: use FolderUID instead */ + folderId?: number; + folderUid?: string; + inputs?: ImportDashboardInputDefinitionOfInputParametersWhenImportingADashboard[]; + overwrite?: boolean; + path?: string; + pluginId?: string; +}; +export type PublicDashboardListResponse = { + accessToken?: string; + dashboardUid?: string; + isEnabled?: boolean; + slug?: string; + title?: string; + uid?: string; +}; +export type PublicDashboardListResponseWithPagination = { + page?: number; + perPage?: number; + publicDashboards?: PublicDashboardListResponse[]; + totalCount?: number; +}; +export type PublicError2 = { + /** Extra Additional information about the error */ + extra?: { + [key: string]: any; + }; + /** Message A human readable message */ + message?: string; + /** MessageID A unique identifier for the error */ + messageId: string; + /** StatusCode The HTTP status code returned */ + statusCode: number; +}; +export type DashboardTagCloudItem = { + count?: number; + term?: string; +}; +export type EmailDto = { + recipient?: string; + uid?: string; +}; +export type ShareType = string; +export type PublicDashboard = { + accessToken?: string; + annotationsEnabled?: boolean; + createdAt?: string; + createdBy?: number; + dashboardUid?: string; + isEnabled?: boolean; + recipients?: EmailDto[]; + share?: ShareType; + timeSelectionEnabled?: boolean; + uid?: string; + updatedAt?: string; + updatedBy?: number; +}; +export type PublicDashboardDto = { + accessToken?: string; + annotationsEnabled?: boolean; + isEnabled?: boolean; + share?: ShareType; + timeSelectionEnabled?: boolean; + uid?: string; +}; +export type DashboardFullWithMeta = { + dashboard?: Json; + meta?: DashboardMeta; +}; +export type PermissionType = number; +export type DashboardAclInfoDto = { + created?: string; + dashboardId?: number; + /** Deprecated: use FolderUID instead */ + folderId?: number; + folderUid?: string; + inherited?: boolean; + isFolder?: boolean; + permission?: PermissionType; + permissionName?: string; + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; + slug?: string; + team?: string; + teamAvatarUrl?: string; + teamEmail?: string; + teamId?: number; + teamUid?: string; + title?: string; + uid?: string; + updated?: string; + url?: string; + userAvatarUrl?: string; + userEmail?: string; + userId?: number; + userLogin?: string; + userUid?: string; +}; +export type DashboardAclUpdateItem = { + permission?: PermissionType; + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; + teamId?: number; + userId?: number; +}; +export type UpdateDashboardAclCommand = { + items?: DashboardAclUpdateItem[]; +}; +export type RestoreDashboardVersionCommand = { + version?: number; +}; +export type DashboardVersionMeta = { + created?: string; + createdBy?: string; + dashboardId?: number; + data?: Json; + id?: number; + message?: string; + parentVersion?: number; + restoredFrom?: number; + uid?: string; + version?: number; +}; +export type DashboardVersionResponseMeta = { + continueToken?: string; + versions?: DashboardVersionMeta[]; +}; +export type DsAccess = string; +export type DataSourceListItemDto = { + access?: DsAccess; + basicAuth?: boolean; + database?: string; + id?: number; + isDefault?: boolean; + jsonData?: Json; + name?: string; + orgId?: number; + readOnly?: boolean; + type?: string; + typeLogoUrl?: string; + typeName?: string; + uid?: string; + url?: string; + user?: string; +}; +export type DataSourceList = DataSourceListItemDto[]; +export type Metadata = { + [key: string]: boolean; +}; +export type DataSource = { + access?: DsAccess; + accessControl?: Metadata; + basicAuth?: boolean; + basicAuthUser?: string; + database?: string; + id?: number; + isDefault?: boolean; + jsonData?: Json; + name?: string; + orgId?: number; + readOnly?: boolean; + secureJsonFields?: { + [key: string]: boolean; + }; + type?: string; + typeLogoUrl?: string; + uid?: string; + url?: string; + user?: string; + version?: number; + withCredentials?: boolean; +}; +export type AddDataSourceCommand = { + access?: DsAccess; + basicAuth?: boolean; + basicAuthUser?: string; + database?: string; + isDefault?: boolean; + jsonData?: Json; + name?: string; + secureJsonData?: { + [key: string]: string; + }; + type?: string; + uid?: string; + url?: string; + user?: string; + withCredentials?: boolean; +}; +export type Transformation = { + expression?: string; + field?: string; + mapValue?: string; + type?: 'regex' | 'logfmt'; +}; +export type Transformations = Transformation[]; +export type CorrelationType = string; +export type CorrelationConfig = { + /** Field used to attach the correlation link */ + field: string; + /** Target data query */ + target: { + [key: string]: any; + }; + transformations?: Transformations; + type?: CorrelationType; +}; +export type Correlation = { + config?: CorrelationConfig; + /** Description of the correlation */ + description?: string; + /** Label identifying the correlation */ + label?: string; + /** OrgID of the data source the correlation originates from */ + orgId?: number; + /** Provisioned True if the correlation was created during provisioning */ + provisioned?: boolean; + /** UID of the data source the correlation originates from */ + sourceUID?: string; + /** UID of the data source the correlation points to */ + targetUID?: string; + type?: CorrelationType; + /** Unique identifier of the correlation */ + uid?: string; +}; +export type CreateCorrelationResponseBody = { + message?: string; + result?: Correlation; +}; +export type CreateCorrelationCommand = { + config?: CorrelationConfig; + /** Optional description of the correlation */ + description?: string; + /** Optional label identifying the correlation */ + label?: string; + /** True if correlation was created with provisioning. This makes it read-only. */ + provisioned?: boolean; + /** Target data source UID to which the correlation is created. required if type = query */ + targetUID?: string; + type?: CorrelationType; +}; +export type UpdateCorrelationResponseBody = { + message?: string; + result?: Correlation; +}; +export type CorrelationConfigUpdateDto = { + /** Field used to attach the correlation link */ + field?: string; + /** Target data query */ + target?: { + [key: string]: any; + }; + /** Source data transformations */ + transformations?: Transformation[]; +}; +export type UpdateCorrelationCommand = { + config?: CorrelationConfigUpdateDto; + /** Optional description of the correlation */ + description?: string; + /** Optional label identifying the correlation */ + label?: string; + type?: CorrelationType; +}; +export type UpdateDataSourceCommand = { + access?: DsAccess; + basicAuth?: boolean; + basicAuthUser?: string; + database?: string; + isDefault?: boolean; + jsonData?: Json; + name?: string; + secureJsonData?: { + [key: string]: string; + }; + type?: string; + uid?: string; + url?: string; + user?: string; + /** The previous version -- used for optimistic locking */ + version?: number; + withCredentials?: boolean; +}; +export type DeleteCorrelationResponseBody = { + message?: string; +}; +export type TeamLbacRule = { + rules?: string[]; + teamId?: string; + teamUid?: string; +}; +export type TeamLbacRules = { + rules?: TeamLbacRule[]; +}; +export type UpdateTeamLbacCommand = { + rules?: TeamLbacRule[]; +}; +export type CacheConfigResponse = { + created?: string; + /** Fields that can be set by the API caller - read/write */ + dataSourceID?: number; + dataSourceUID?: string; + /** These are returned by the HTTP API, but are managed internally - read-only + Note: 'created' and 'updated' are special properties managed automatically by xorm, but we are setting them manually */ + defaultTTLMs?: number; + enabled?: boolean; + message?: string; + /** TTL MS, or "time to live", is how long a cached item will stay in the cache before it is removed (in milliseconds) */ + ttlQueriesMs?: number; + ttlResourcesMs?: number; + updated?: string; + /** If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini */ + useDefaultTTL?: boolean; +}; +export type CacheConfigSetter = { + dataSourceID?: number; + dataSourceUID?: string; + enabled?: boolean; + /** TTL MS, or "time to live", is how long a cached item will stay in the cache before it is removed (in milliseconds) */ + ttlQueriesMs?: number; + ttlResourcesMs?: number; + /** If UseDefaultTTL is enabled, then the TTLQueriesMS and TTLResourcesMS in this object is always sent as the default TTL located in grafana.ini */ + useDefaultTTL?: boolean; +}; +export type SourceTypeDefinesTheStatusSource = string; +export type ExplorePanelsState = any; +export type TimeRange = { + from?: string; + to?: string; +}; +export type SupportedTransformationTypes = string; +export type LinkTransformationConfig = { + expression?: string; + field?: string; + mapValue?: string; + type?: SupportedTransformationTypes; +}; +export type InternalDataLink = { + datasourceName?: string; + datasourceUid?: string; + panelsState?: ExplorePanelsState; + query?: any; + timeRange?: TimeRange; + transformations?: LinkTransformationConfig[]; +}; +export type DataLink = { + internal?: InternalDataLink; + targetBlank?: boolean; + title?: string; + url?: string; +}; +export type ValueMapping = object; +export type ValueMappings = ValueMapping[]; +export type ConfFloat64 = number; +export type ThresholdsMode = string; +export type Threshold = { + color?: string; + state?: string; + value?: ConfFloat64; +}; +export type ThresholdsConfig = { + mode?: ThresholdsMode; + /** Must be sorted by 'value', first value is always -Infinity */ + steps?: Threshold[]; +}; +export type EnumFieldConfig = { + /** Color is the color value for a given index (empty is undefined) */ + color?: string[]; + /** Description of the enum state */ + description?: string[]; + /** Icon supports setting an icon for a given index value */ + icon?: string[]; + /** Value is the string display value for a given index */ + text?: string[]; +}; +export type FieldTypeConfig = { + enum?: EnumFieldConfig; +}; +export type FieldConfigRepresentsTheDisplayPropertiesForAField = { + /** Map values to a display color + NOTE: this interface is under development in the frontend... so simple map for now */ + color?: { + [key: string]: any; + }; + /** Panel Specific Values */ + custom?: { + [key: string]: any; + }; + decimals?: number; + /** Description is human readable field metadata */ + description?: string; + /** DisplayName overrides Grafana default naming, should not be used from a data source */ + displayName?: string; + /** DisplayNameFromDS overrides Grafana default naming strategy. */ + displayNameFromDS?: string; + /** Filterable indicates if the Field's data can be filtered by additional calls. */ + filterable?: boolean; + /** Interval indicates the expected regular step between values in the series. + When an interval exists, consumers can identify "missing" values when the expected value is not present. + The grafana timeseries visualization will render disconnected values when missing values are found it the time field. + The interval uses the same units as the values. For time.Time, this is defined in milliseconds. */ + interval?: number; + /** The behavior when clicking on a result */ + links?: DataLink[]; + mappings?: ValueMappings; + max?: ConfFloat64; + min?: ConfFloat64; + /** Alternative to empty string */ + noValue?: string; + /** Path is an explicit path to the field in the datasource. When the frame meta includes a path, + this will default to `${frame.meta.path}/${field.name} + + When defined, this value can be used as an identifier within the datasource scope, and + may be used as an identifier to update values in a subsequent request */ + path?: string; + thresholds?: ThresholdsConfig; + type?: FieldTypeConfig; + /** Numeric Options */ + unit?: string; + /** Writeable indicates that the datasource knows how to update this value */ + writeable?: boolean; +}; +export type FrameLabels = { + [key: string]: string; +}; +export type FieldRepresentsATypedColumnOfDataWithinAFrame = { + config?: FieldConfigRepresentsTheDisplayPropertiesForAField; + labels?: FrameLabels; + /** Name is default identifier of the field. The name does not have to be unique, but the combination + of name and Labels should be unique for proper behavior in all situations. */ + name?: string; +}; +export type DataTopicIsUsedToIdentifyWhichTopicTheFrameShouldBeAssignedTo = string; +export type InspectTypeIsATypeForTheInspectPropertyOfANotice = number; +export type NoticeSeverityIsATypeForTheSeverityPropertyOfANotice = number; +export type NoticeProvidesAStructureForPresentingNotificationsInGrafanasUserInterface = { + inspect?: InspectTypeIsATypeForTheInspectPropertyOfANotice; + /** Link is an optional link for display in the user interface and can be an + absolute URL or a path relative to Grafana's root url. */ + link?: string; + severity?: NoticeSeverityIsATypeForTheSeverityPropertyOfANotice; + /** Text is freeform descriptive text for the notice. */ + text?: string; +}; +export type VisTypeIsUsedToIndicateHowTheDataShouldBeVisualizedInExplore = string; +export type QueryStatIsUsedForStoringArbitraryStatisticsMetadataRelatedToAQueryAndItsResultEGTotalRequestTimeDataProcessingTime = + { + /** Map values to a display color + NOTE: this interface is under development in the frontend... so simple map for now */ + color?: { + [key: string]: any; + }; + /** Panel Specific Values */ + custom?: { + [key: string]: any; + }; + decimals?: number; + /** Description is human readable field metadata */ + description?: string; + /** DisplayName overrides Grafana default naming, should not be used from a data source */ + displayName?: string; + /** DisplayNameFromDS overrides Grafana default naming strategy. */ + displayNameFromDS?: string; + /** Filterable indicates if the Field's data can be filtered by additional calls. */ + filterable?: boolean; + /** Interval indicates the expected regular step between values in the series. + When an interval exists, consumers can identify "missing" values when the expected value is not present. + The grafana timeseries visualization will render disconnected values when missing values are found it the time field. + The interval uses the same units as the values. For time.Time, this is defined in milliseconds. */ + interval?: number; + /** The behavior when clicking on a result */ + links?: DataLink[]; + mappings?: ValueMappings; + max?: ConfFloat64; + min?: ConfFloat64; + /** Alternative to empty string */ + noValue?: string; + /** Path is an explicit path to the field in the datasource. When the frame meta includes a path, + this will default to `${frame.meta.path}/${field.name} + + When defined, this value can be used as an identifier within the datasource scope, and + may be used as an identifier to update values in a subsequent request */ + path?: string; + thresholds?: ThresholdsConfig; + type?: FieldTypeConfig; + /** Numeric Options */ + unit?: string; + value?: number; + /** Writeable indicates that the datasource knows how to update this value */ + writeable?: boolean; + }; +export type FrameType = string; +export type FrameTypeIsA2NumberVersionMajorMinor = number[]; +export type FrameMetaMatches = { + /** Channel is the path to a stream in grafana live that has real-time updates for this data. */ + channel?: string; + /** Custom datasource specific values. */ + custom?: any; + dataTopic?: DataTopicIsUsedToIdentifyWhichTopicTheFrameShouldBeAssignedTo; + /** ExecutedQueryString is the raw query sent to the underlying system. All macros and templating + have been applied. When metadata contains this value, it will be shown in the query inspector. */ + executedQueryString?: string; + /** Notices provide additional information about the data in the Frame that + Grafana can display to the user in the user interface. */ + notices?: NoticeProvidesAStructureForPresentingNotificationsInGrafanasUserInterface[]; + /** Path is a browsable path on the datasource. */ + path?: string; + /** PathSeparator defines the separator pattern to decode a hierarchy. The default separator is '/'. */ + pathSeparator?: string; + /** PreferredVisualizationPluginId sets the panel plugin id to use to render the data when using Explore. If + the plugin cannot be found will fall back to PreferredVisualization. */ + preferredVisualisationPluginId?: string; + preferredVisualisationType?: VisTypeIsUsedToIndicateHowTheDataShouldBeVisualizedInExplore; + /** Stats is an array of query result statistics. */ + stats?: QueryStatIsUsedForStoringArbitraryStatisticsMetadataRelatedToAQueryAndItsResultEGTotalRequestTimeDataProcessingTime[]; + type?: FrameType; + typeVersion?: FrameTypeIsA2NumberVersionMajorMinor; + /** Array of field indices which values create a unique id for each row. Ideally this should be globally unique ID + but that isn't guarantied. Should help with keeping track and deduplicating rows in visualizations, especially + with streaming data with frequent updates. */ + uniqueRowIdFields?: number[]; +}; +export type FrameIsAColumnarDataStructureWhereEachColumnIsAField = { + /** Fields are the columns of a frame. + All Fields must be of the same the length when marshalling the Frame for transmission. + There should be no `nil` entries in the Fields slice (making them pointers was a mistake). */ + Fields?: FieldRepresentsATypedColumnOfDataWithinAFrame[]; + Meta?: FrameMetaMatches; + /** Name is used in some Grafana visualizations. */ + Name?: string; + /** RefID is a property that can be set to match a Frame to its originating query. */ + RefID?: string; +}; +export type FramesIsASliceOfFramePointers = FrameIsAColumnarDataStructureWhereEachColumnIsAField[]; +export type DataResponseContainsTheResultsFromADataQuery = { + /** Error is a property to be set if the corresponding DataQuery has an error. */ + Error?: string; + ErrorSource?: SourceTypeDefinesTheStatusSource; + Frames?: FramesIsASliceOfFramePointers; + Status?: Status; +}; +export type ResponsesIsAMapOfRefIDsUniqueQueryIdToDataResponses = { + [key: string]: DataResponseContainsTheResultsFromADataQuery; +}; +export type QueryDataResponseContainsTheResultsFromAQueryDataRequest = { + results?: ResponsesIsAMapOfRefIDsUniqueQueryIdToDataResponses; +}; +export type MetricRequest = { + debug?: boolean; + /** From Start time in epoch timestamps in milliseconds or relative using Grafana time units. */ + from: string; + /** queries.refId – Specifies an identifier of the query. Is optional and default to “A”. + queries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId. + queries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100. + queries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000. */ + queries: Json[]; + /** To End time in epoch timestamps in milliseconds or relative using Grafana time units. */ + to: string; +}; +export type ManagerKindIsTheTypeOfManagerWhichIsResponsibleForManagingTheResource = string; +export type FolderSearchHit = { + id?: number; + managedBy?: ManagerKindIsTheTypeOfManagerWhichIsResponsibleForManagingTheResource; + parentUid?: string; + title?: string; + uid?: string; +}; +export type Folder = { + accessControl?: Metadata; + canAdmin?: boolean; + canDelete?: boolean; + canEdit?: boolean; + canSave?: boolean; + created?: string; + createdBy?: string; + hasAcl?: boolean; + /** Deprecated: use UID instead */ + id?: number; + managedBy?: ManagerKindIsTheTypeOfManagerWhichIsResponsibleForManagingTheResource; + orgId?: number; + /** only used if nested folders are enabled */ + parentUid?: string; + /** the parent folders starting from the root going down */ + parents?: Folder[]; + title?: string; + uid?: string; + updated?: string; + updatedBy?: string; + url?: string; + version?: number; +}; +export type CreateFolderCommand = { + description?: string; + parentUid?: string; + title?: string; + uid?: string; +}; +export type UpdateFolderCommand = { + /** NewDescription it's an optional parameter used for overriding the existing folder description */ + description?: string; + /** Overwrite only used by the legacy folder implementation */ + overwrite?: boolean; + /** NewTitle it's an optional parameter used for overriding the existing folder title */ + title?: string; + /** Version only used by the legacy folder implementation */ + version?: number; +}; +export type DescendantCounts = { + [key: string]: number; +}; +export type MoveFolderCommand = { + parentUid?: string; +}; +export type Group = { + groupID?: string; + mappings?: any; +}; +export type GetGroupsResponse = { + groups?: Group[]; + total?: number; +}; +export type MessageResponse = { + message?: string; +}; +export type GroupAttributes = { + roles?: string[]; +}; +export type HealthResponse = { + commit?: string; + database?: string; + enterpriseCommit?: string; + version?: string; +}; +export type LibraryElementDtoMetaUser = { + avatarUrl?: string; + id?: number; + name?: string; +}; +export type LibraryElementDtoMetaIsTheMetaInformationForLibraryElementDto = { + connectedDashboards?: number; + created?: string; + createdBy?: LibraryElementDtoMetaUser; + folderName?: string; + folderUid?: string; + updated?: string; + updatedBy?: LibraryElementDtoMetaUser; +}; +export type LibraryElementDtoIsTheFrontendDtoForEntities = { + description?: string; + /** Deprecated: use FolderUID instead */ + folderId?: number; + folderUid?: string; + id?: number; + kind?: number; + meta?: LibraryElementDtoMetaIsTheMetaInformationForLibraryElementDto; + model?: object; + name?: string; + orgId?: number; + schemaVersion?: number; + type?: string; + uid?: string; + version?: number; +}; +export type LibraryElementSearchResultIsTheSearchResultForEntities = { + elements?: LibraryElementDtoIsTheFrontendDtoForEntities[]; + page?: number; + perPage?: number; + totalCount?: number; +}; +export type LibraryElementSearchResponseIsAResponseStructForLibraryElementSearchResult = { + result?: LibraryElementSearchResultIsTheSearchResultForEntities; +}; +export type LibraryElementResponseIsAResponseStructForLibraryElementDto = { + result?: LibraryElementDtoIsTheFrontendDtoForEntities; +}; +export type CreateLibraryElementCommand = { + /** ID of the folder where the library element is stored. + + Deprecated: use FolderUID instead */ + folderId?: number; + /** UID of the folder where the library element is stored. */ + folderUid?: string; + /** Kind of element to create, Use 1 for library panels or 2 for c. + Description: + 1 - library panels */ + kind?: 1; + /** The JSON model for the library element. */ + model?: object; + /** Name of the library element. */ + name?: string; + uid?: string; +}; +export type LibraryElementArrayResponseIsAResponseStructForAnArrayOfLibraryElementDto = { + result?: LibraryElementDtoIsTheFrontendDtoForEntities[]; +}; +export type PatchLibraryElementCommand = { + /** ID of the folder where the library element is stored. + + Deprecated: use FolderUID instead */ + folderId?: number; + /** UID of the folder where the library element is stored. */ + folderUid?: string; + /** Kind of element to create, Use 1 for library panels or 2 for c. + Description: + 1 - library panels */ + kind?: 1; + /** The JSON model for the library element. */ + model?: object; + /** Name of the library element. */ + name?: string; + uid?: string; + /** Version of the library element you are updating. */ + version?: number; +}; +export type LibraryElementConnectionDtoIsTheFrontendDtoForElementConnections = { + connectionId?: number; + connectionUid?: string; + created?: string; + createdBy?: LibraryElementDtoMetaUser; + elementId?: number; + /** Deprecated: this field will be removed in the future */ + id?: number; + kind?: number; +}; +export type LibraryElementConnectionsResponseIsAResponseStructForAnArrayOfLibraryElementConnectionDto = { + result?: LibraryElementConnectionDtoIsTheFrontendDtoForElementConnections[]; +}; +export type ActiveUserStats = { + active_admins_and_editors?: number; + active_anonymous_devices?: number; + active_users?: number; + active_viewers?: number; +}; +export type DeleteTokenCommand = { + instance?: string; +}; +export type TokenStatus = number; +export type Token = { + account?: string; + anonymousRatio?: number; + company?: string; + details_url?: string; + exp?: number; + iat?: number; + included_users?: number; + iss?: string; + jti?: string; + lexp?: number; + lic_exp_warn_days?: number; + lid?: string; + limit_by?: string; + max_concurrent_user_sessions?: number; + nbf?: number; + prod?: string[]; + slug?: string; + status?: TokenStatus; + sub?: string; + tok_exp_warn_days?: number; + trial?: boolean; + trial_exp?: number; + update_days?: number; + usage_billing?: boolean; +}; +export type Address = { + address1?: string; + address2?: string; + city?: string; + country?: string; + state?: string; + zipCode?: string; +}; +export type OrgDetailsDto = { + address?: Address; + id?: number; + name?: string; +}; +export type UpdateOrgForm = { + name?: string; +}; +export type UpdateOrgAddressForm = { + address1?: string; + address2?: string; + city?: string; + country?: string; + state?: string; + zipcode?: string; +}; +export type TempUserStatus = string; +export type TempUserDto = { + code?: string; + createdOn?: string; + email?: string; + emailSent?: boolean; + emailSentOn?: string; + id?: number; + invitedByEmail?: string; + invitedByLogin?: string; + invitedByName?: string; + name?: string; + orgId?: number; + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; + status?: TempUserStatus; + url?: string; +}; +export type AddInviteForm = { + loginOrEmail?: string; + name?: string; + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; + sendEmail?: boolean; +}; +export type PreferencesCookiePreferences = { + analytics?: any; + functional?: any; + performance?: any; +}; +export type PreferencesNavbarPreference = { + bookmarkUrls?: string[]; +}; +export type PreferencesQueryHistoryPreference = { + /** one of: '' | 'query' | 'starred'; */ + homeTab?: string; +}; +export type PreferencesSpec = { + cookiePreferences?: PreferencesCookiePreferences; + /** UID for the home dashboard */ + homeDashboardUID?: string; + /** Selected language (beta) */ + language?: string; + navbar?: PreferencesNavbarPreference; + queryHistory?: PreferencesQueryHistoryPreference; + /** Selected locale (beta) */ + regionalFormat?: string; + /** light, dark, empty is default */ + theme?: string; + /** The timezone selection + TODO: this should use the timezone defined in common */ + timezone?: string; + /** day of the week (sunday, monday, etc) */ + weekStart?: string; +}; +export type CookieType = string; +export type NavbarPreference = { + bookmarkUrls?: string[]; +}; +export type QueryHistoryPreference = { + homeTab?: string; +}; +export type PatchPrefsCmd = { + cookies?: CookieType[]; + /** The numerical :id of a favorited dashboard */ + homeDashboardId?: number; + homeDashboardUID?: string; + language?: string; + navbar?: NavbarPreference; + queryHistory?: QueryHistoryPreference; + regionalFormat?: string; + theme?: 'light' | 'dark'; + timezone?: 'utc' | 'browser'; + weekStart?: string; +}; +export type UpdatePrefsCmd = { + cookies?: CookieType[]; + /** The numerical :id of a favorited dashboard */ + homeDashboardId?: number; + homeDashboardUID?: string; + language?: string; + navbar?: NavbarPreference; + queryHistory?: QueryHistoryPreference; + regionalFormat?: string; + theme?: 'light' | 'dark' | 'system'; + timezone?: 'utc' | 'browser'; + weekStart?: string; +}; +export type OrgUserDto = { + accessControl?: { + [key: string]: boolean; + }; + authLabels?: string[]; + avatarUrl?: string; + email?: string; + isDisabled?: boolean; + isExternallySynced?: boolean; + isProvisioned?: boolean; + lastSeenAt?: string; + lastSeenAtAge?: string; + login?: string; + name?: string; + orgId?: number; + role?: string; + uid?: string; + userId?: number; +}; +export type AddOrgUserCommand = { + loginOrEmail?: string; + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; +}; +export type UserLookupDto = { + avatarUrl?: string; + login?: string; + uid?: string; + userId?: number; +}; +export type UpdateOrgUserCommand = { + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; +}; +export type OrgDto = { + id?: number; + name?: string; +}; +export type CreateOrgCommand = { + name?: string; +}; +export type SearchOrgUsersQueryResult = { + orgUsers?: OrgUserDto[]; + page?: number; + perPage?: number; + totalCount?: number; +}; +export type Playlist = { + id?: number; + interval?: string; + name?: string; + uid?: string; +}; +export type Playlists = Playlist[]; +export type PlaylistItem = { + Id?: number; + PlaylistId?: number; + order?: number; + title?: string; + type?: string; + value?: string; +}; +export type CreatePlaylistCommand = { + interval?: string; + items?: PlaylistItem[]; + name?: string; +}; +export type PlaylistItemDto = { + /** Title is an unused property -- it will be removed in the future */ + title?: string; + /** Type of the item. */ + type?: string; + /** Value depends on type and describes the playlist item. + + dashboard_by_id: The value is an internal numerical identifier set by Grafana. This + is not portable as the numerical identifier is non-deterministic between different instances. + Will be replaced by dashboard_by_uid in the future. (deprecated) + dashboard_by_tag: The value is a tag which is set on any number of dashboards. All + dashboards behind the tag will be added to the playlist. + dashboard_by_uid: The value is the dashboard UID */ + value?: string; +}; +export type PlaylistDto = { + /** Interval sets the time between switching views in a playlist. */ + interval?: string; + /** The ordered list of items that the playlist will iterate over. */ + items?: PlaylistItemDto[]; + /** Name of the playlist. */ + name?: string; + /** Unique playlist identifier. Generated on creation, either by the + creator of the playlist of by the application. */ + uid?: string; +}; +export type UpdatePlaylistCommand = { + interval?: string; + items?: PlaylistItem[]; + name?: string; + uid?: string; +}; +export type DataSourceRef = { + /** The plugin type-id */ + type?: string; + /** Specific datasource instance */ + uid?: string; +}; +export type AnnotationPanelFilter = { + /** Should the specified panels be included or excluded */ + exclude?: boolean; + /** Panel IDs that should be included or excluded */ + ids?: number[]; +}; +export type AnnotationTarget = { + /** Only required/valid for the grafana datasource... + but code+tests is already depending on it so hard to change */ + limit?: number; + /** Only required/valid for the grafana datasource... + but code+tests is already depending on it so hard to change */ + matchAny?: boolean; + /** Only required/valid for the grafana datasource... + but code+tests is already depending on it so hard to change */ + tags?: string[]; + /** Only required/valid for the grafana datasource... + but code+tests is already depending on it so hard to change */ + type?: string; +}; +export type AnnotationQuery = { + /** Set to 1 for the standard annotation query all dashboards have by default. */ + builtIn?: number; + datasource?: DataSourceRef; + /** When enabled the annotation query is issued with every dashboard refresh */ + enable?: boolean; + filter?: AnnotationPanelFilter; + /** Annotation queries can be toggled on or off at the top of the dashboard. + When hide is true, the toggle is not shown in the dashboard. */ + hide?: boolean; + /** Color to use for the annotation event markers */ + iconColor?: string; + /** Name of annotation. */ + name?: string; + /** Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. */ + placement?: string; + target?: AnnotationTarget; + /** TODO -- this should not exist here, it is based on the --grafana-- datasource */ + type?: string; +}; +export type AnnotationEvent = { + color?: string; + dashboardId?: number; + dashboardUID?: string; + id?: number; + isRegion?: boolean; + panelId?: number; + source?: AnnotationQuery; + tags?: string[]; + text?: string; + time?: number; + timeEnd?: number; +}; +export type QueryHistoryDto = { + comment?: string; + createdAt?: number; + createdBy?: number; + datasourceUid?: string; + queries?: Json; + starred?: boolean; + uid?: string; +}; +export type QueryHistorySearchResult = { + page?: number; + perPage?: number; + queryHistory?: QueryHistoryDto[]; + totalCount?: number; +}; +export type QueryHistorySearchResponse = { + result?: QueryHistorySearchResult; +}; +export type QueryHistoryResponse = { + result?: QueryHistoryDto; +}; +export type CreateQueryInQueryHistoryCommand = { + /** UID of the data source for which are queries stored. */ + datasourceUid?: string; + queries: Json; +}; +export type QueryHistoryDeleteQueryResponse = { + id?: number; + message?: string; +}; +export type PatchQueryCommentInQueryHistoryCommand = { + /** Updated comment */ + comment?: string; +}; +export type RecordingRuleJson = { + active?: boolean; + count?: boolean; + description?: string; + dest_data_source_uid?: string; + id?: string; + interval?: number; + name?: string; + prom_name?: string; + queries?: { + [key: string]: any; + }[]; + range?: number; + target_ref_id?: string; +}; +export type PrometheusRemoteWriteTargetJson = { + data_source_uid?: string; + id?: string; + remote_write_path?: string; +}; +export type ReportDashboardId = { + id?: number; + name?: string; + uid?: string; +}; +export type ReportTimeRange = { + from?: string; + to?: string; +}; +export type ReportDashboard = { + dashboard?: ReportDashboardId; + reportVariables?: object; + timeRange?: ReportTimeRange; +}; +export type Type = string; +export type ReportOptions = { + layout?: string; + orientation?: string; + pdfCombineOneFile?: boolean; + pdfShowTemplateVariables?: boolean; + timeRange?: ReportTimeRange; +}; +export type ReportSchedule = { + dayOfMonth?: string; + endDate?: string; + frequency?: string; + intervalAmount?: number; + intervalFrequency?: string; + startDate?: string; + timeZone?: string; + workdaysOnly?: boolean; +}; +export type State = string; +export type Report = { + created?: string; + dashboards?: ReportDashboard[]; + enableCsv?: boolean; + enableDashboardUrl?: boolean; + formats?: Type[]; + id?: number; + message?: string; + name?: string; + options?: ReportOptions; + orgId?: number; + recipients?: string; + replyTo?: string; + scaleFactor?: number; + schedule?: ReportSchedule; + state?: State; + subject?: string; + uid?: string; + updated?: string; + userId?: number; +}; +export type CreateOrUpdateReport = { + dashboards?: ReportDashboard[]; + enableCsv?: boolean; + enableDashboardUrl?: boolean; + formats?: Type[]; + message?: string; + name?: string; + options?: ReportOptions; + recipients?: string; + replyTo?: string; + scaleFactor?: number; + schedule?: ReportSchedule; + state?: State; + subject?: string; +}; +export type ReportEmail = { + /** Comma-separated list of emails to which to send the report to. */ + emails?: string; + /** Send the report to the emails specified in the report. Required if emails is not present. */ + id?: string; + /** Send the report to the emails specified in the report. Required if emails is not present. */ + useEmailsFromReport?: boolean; +}; +export type ReportBrandingOptions = { + emailFooterLink?: string; + emailFooterMode?: string; + emailFooterText?: string; + emailLogoUrl?: string; + reportLogoUrl?: string; +}; +export type ReportSettings = { + branding?: ReportBrandingOptions; + embeddedImageTheme?: string; + id?: number; + orgId?: number; + pdfTheme?: string; + userId?: number; +}; +export type HitType = string; +export type Hit = { + description?: string; + folderId?: number; + folderTitle?: string; + folderUid?: string; + folderUrl?: string; + id?: number; + isDeleted?: boolean; + isStarred?: boolean; + orgId?: number; + permanentlyDeleteDate?: string; + slug?: string; + sortMeta?: number; + sortMetaName?: string; + tags?: string[]; + title?: string; + type?: HitType; + uid?: string; + uri?: string; + url?: string; +}; +export type HitList = Hit[]; +export type ServiceAccountDto = { + accessControl?: { + [key: string]: boolean; + }; + avatarUrl?: string; + id?: number; + isDisabled?: boolean; + isExternal?: boolean; + login?: string; + name?: string; + orgId?: number; + role?: string; + tokens?: number; + uid?: string; +}; +export type CreateServiceAccountForm = { + isDisabled?: boolean; + name?: string; + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; +}; +export type SearchOrgServiceAccountsResult = { + page?: number; + perPage?: number; + serviceAccounts?: ServiceAccountDto[]; + /** It can be used for pagination of the user list + E.g. if totalCount is equal to 100 users and + the perpage parameter is set to 10 then there are 10 pages of users. */ + totalCount?: number; +}; +export type ServiceAccountProfileDto = { + accessControl?: { + [key: string]: boolean; + }; + avatarUrl?: string; + createdAt?: string; + id?: number; + isDisabled?: boolean; + isExternal?: boolean; + login?: string; + name?: string; + orgId?: number; + requiredBy?: string; + role?: string; + teams?: string[]; + tokens?: number; + uid?: string; + updatedAt?: string; +}; +export type UpdateServiceAccountForm = { + isDisabled?: boolean; + name?: string; + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; + serviceAccountId?: number; +}; +export type TokenDto = { + created?: string; + expiration?: string; + hasExpired?: boolean; + id?: number; + isRevoked?: boolean; + lastUsedAt?: string; + name?: string; + secondsUntilExpiration?: number; +}; +export type NewApiKeyResult = { + id?: number; + key?: string; + name?: string; +}; +export type AddServiceAccountTokenCommand = { + name?: string; + secondsToLive?: number; +}; +export type AnIpMaskIsABitmaskThatCanBeUsedToManipulateIpAddressesForIpAddressingAndRouting = number[]; +export type AnIpNetRepresentsAnIpNetwork = { + IP?: string; + Mask?: AnIpMaskIsABitmaskThatCanBeUsedToManipulateIpAddressesForIpAddressingAndRouting; +}; +export type ExtKeyUsageRepresentsAnExtendedSetOfActionsThatAreValidForAGivenKey = number; +export type AnObjectIdentifierRepresentsAnAsn1ObjectIdentifier = number[]; +export type Extension = { + Critical?: boolean; + Id?: AnObjectIdentifierRepresentsAnAsn1ObjectIdentifier; + Value?: number[]; +}; +export type AttributeTypeAndValue = { + Type?: AnObjectIdentifierRepresentsAnAsn1ObjectIdentifier; + Value?: any; +}; +export type Name = { + Country?: string[]; + /** ExtraNames contains attributes to be copied, raw, into any marshaled + distinguished names. Values override any attributes with the same OID. + The ExtraNames field is not populated when parsing, see Names. */ + ExtraNames?: AttributeTypeAndValue[]; + Locality?: string[]; + /** Names contains all parsed attributes. When parsing distinguished names, + this can be used to extract non-standard attributes that are not parsed + by this package. When marshaling to RDNSequences, the Names field is + ignored, see ExtraNames. */ + Names?: AttributeTypeAndValue[]; + SerialNumber?: string; + StreetAddress?: string[]; +}; +export type KeyUsage = number; +export type PolicyMappingRepresentsAPolicyMappingEntryInThePolicyMappingsExtension = { + /** IssuerDomainPolicy contains a policy OID the issuing certificate considers + equivalent to SubjectDomainPolicy in the subject certificate. */ + IssuerDomainPolicy?: string; + /** SubjectDomainPolicy contains a OID the issuing certificate considers + equivalent to IssuerDomainPolicy in the subject certificate. */ + SubjectDomainPolicy?: string; +}; +export type PublicKeyAlgorithm = number; +export type SignatureAlgorithm = number; +export type Userinfo = object; +export type AUrlRepresentsAParsedUrlTechnicallyAUriReference = { + ForceQuery?: boolean; + Fragment?: string; + Host?: string; + OmitHost?: boolean; + Opaque?: string; + Path?: string; + RawFragment?: string; + RawPath?: string; + RawQuery?: string; + Scheme?: string; + User?: Userinfo; +}; +export type ACertificateRepresentsAnX509Certificate = { + AuthorityKeyId?: number[]; + /** BasicConstraintsValid indicates whether IsCA, MaxPathLen, + and MaxPathLenZero are valid. */ + BasicConstraintsValid?: boolean; + /** CRL Distribution Points */ + CRLDistributionPoints?: string[]; + /** Subject Alternate Name values. (Note that these values may not be valid + if invalid values were contained within a parsed certificate. For + example, an element of DNSNames may not be a valid DNS domain name.) */ + DNSNames?: string[]; + EmailAddresses?: string[]; + ExcludedDNSDomains?: string[]; + ExcludedEmailAddresses?: string[]; + ExcludedIPRanges?: AnIpNetRepresentsAnIpNetwork[]; + ExcludedURIDomains?: string[]; + ExtKeyUsage?: ExtKeyUsageRepresentsAnExtendedSetOfActionsThatAreValidForAGivenKey[]; + /** Extensions contains raw X.509 extensions. When parsing certificates, + this can be used to extract non-critical extensions that are not + parsed by this package. When marshaling certificates, the Extensions + field is ignored, see ExtraExtensions. */ + Extensions?: Extension[]; + /** ExtraExtensions contains extensions to be copied, raw, into any + marshaled certificates. Values override any extensions that would + otherwise be produced based on the other fields. The ExtraExtensions + field is not populated when parsing certificates, see Extensions. */ + ExtraExtensions?: Extension[]; + IPAddresses?: string[]; + /** InhibitAnyPolicy and InhibitAnyPolicyZero indicate the presence and value + of the inhibitAnyPolicy extension. + + The value of InhibitAnyPolicy indicates the number of additional + certificates in the path after this certificate that may use the + anyPolicy policy OID to indicate a match with any other policy. + + When parsing a certificate, a positive non-zero InhibitAnyPolicy means + that the field was specified, -1 means it was unset, and + InhibitAnyPolicyZero being true mean that the field was explicitly set to + zero. The case of InhibitAnyPolicy==0 with InhibitAnyPolicyZero==false + should be treated equivalent to -1 (unset). */ + InhibitAnyPolicy?: number; + /** InhibitAnyPolicyZero indicates that InhibitAnyPolicy==0 should be + interpreted as an actual maximum path length of zero. Otherwise, that + combination is interpreted as InhibitAnyPolicy not being set. */ + InhibitAnyPolicyZero?: boolean; + /** InhibitPolicyMapping and InhibitPolicyMappingZero indicate the presence + and value of the inhibitPolicyMapping field of the policyConstraints + extension. + + The value of InhibitPolicyMapping indicates the number of additional + certificates in the path after this certificate that may use policy + mapping. + + When parsing a certificate, a positive non-zero InhibitPolicyMapping + means that the field was specified, -1 means it was unset, and + InhibitPolicyMappingZero being true mean that the field was explicitly + set to zero. The case of InhibitPolicyMapping==0 with + InhibitPolicyMappingZero==false should be treated equivalent to -1 + (unset). */ + InhibitPolicyMapping?: number; + /** InhibitPolicyMappingZero indicates that InhibitPolicyMapping==0 should be + interpreted as an actual maximum path length of zero. Otherwise, that + combination is interpreted as InhibitAnyPolicy not being set. */ + InhibitPolicyMappingZero?: boolean; + IsCA?: boolean; + Issuer?: Name; + IssuingCertificateURL?: string[]; + KeyUsage?: KeyUsage; + /** MaxPathLen and MaxPathLenZero indicate the presence and + value of the BasicConstraints' "pathLenConstraint". + + When parsing a certificate, a positive non-zero MaxPathLen + means that the field was specified, -1 means it was unset, + and MaxPathLenZero being true mean that the field was + explicitly set to zero. The case of MaxPathLen==0 with MaxPathLenZero==false + should be treated equivalent to -1 (unset). + + When generating a certificate, an unset pathLenConstraint + can be requested with either MaxPathLen == -1 or using the + zero value for both MaxPathLen and MaxPathLenZero. */ + MaxPathLen?: number; + /** MaxPathLenZero indicates that BasicConstraintsValid==true + and MaxPathLen==0 should be interpreted as an actual + maximum path length of zero. Otherwise, that combination is + interpreted as MaxPathLen not being set. */ + MaxPathLenZero?: boolean; + NotBefore?: string; + /** RFC 5280, 4.2.2.1 (Authority Information Access) */ + OCSPServer?: string[]; + PermittedDNSDomains?: string[]; + /** Name constraints */ + PermittedDNSDomainsCritical?: boolean; + PermittedEmailAddresses?: string[]; + PermittedIPRanges?: AnIpNetRepresentsAnIpNetwork[]; + PermittedURIDomains?: string[]; + /** Policies contains all policy identifiers included in the certificate. + See CreateCertificate for context about how this field and the PolicyIdentifiers field + interact. + In Go 1.22, encoding/gob cannot handle and ignores this field. */ + Policies?: string[]; + /** PolicyIdentifiers contains asn1.ObjectIdentifiers, the components + of which are limited to int32. If a certificate contains a policy which + cannot be represented by asn1.ObjectIdentifier, it will not be included in + PolicyIdentifiers, but will be present in Policies, which contains all parsed + policy OIDs. + See CreateCertificate for context about how this field and the Policies field + interact. */ + PolicyIdentifiers?: AnObjectIdentifierRepresentsAnAsn1ObjectIdentifier[]; + /** PolicyMappings contains a list of policy mappings included in the certificate. */ + PolicyMappings?: PolicyMappingRepresentsAPolicyMappingEntryInThePolicyMappingsExtension[]; + PublicKey?: any; + PublicKeyAlgorithm?: PublicKeyAlgorithm; + Raw?: number[]; + RawIssuer?: number[]; + RawSubject?: number[]; + RawSubjectPublicKeyInfo?: number[]; + RawTBSCertificate?: number[]; + /** RequireExplicitPolicy and RequireExplicitPolicyZero indicate the presence + and value of the requireExplicitPolicy field of the policyConstraints + extension. + + The value of RequireExplicitPolicy indicates the number of additional + certificates in the path after this certificate before an explicit policy + is required for the rest of the path. When an explicit policy is required, + each subsequent certificate in the path must contain a required policy OID, + or a policy OID which has been declared as equivalent through the policy + mapping extension. + + When parsing a certificate, a positive non-zero RequireExplicitPolicy + means that the field was specified, -1 means it was unset, and + RequireExplicitPolicyZero being true mean that the field was explicitly + set to zero. The case of RequireExplicitPolicy==0 with + RequireExplicitPolicyZero==false should be treated equivalent to -1 + (unset). */ + RequireExplicitPolicy?: number; + /** RequireExplicitPolicyZero indicates that RequireExplicitPolicy==0 should be + interpreted as an actual maximum path length of zero. Otherwise, that + combination is interpreted as InhibitAnyPolicy not being set. */ + RequireExplicitPolicyZero?: boolean; + SerialNumber?: string; + Signature?: number[]; + SignatureAlgorithm?: SignatureAlgorithm; + Subject?: Name; + SubjectKeyId?: number[]; + URIs?: AUrlRepresentsAParsedUrlTechnicallyAUriReference[]; + /** UnhandledCriticalExtensions contains a list of extension IDs that + were not (fully) processed when parsing. Verify will fail if this + slice is non-empty, unless verification is delegated to an OS + library which understands all the critical extensions. + + Users can access these extensions using Extensions and can remove + elements from this slice if they believe that they have been + handled. */ + UnhandledCriticalExtensions?: AnObjectIdentifierRepresentsAnAsn1ObjectIdentifier[]; + UnknownExtKeyUsage?: AnObjectIdentifierRepresentsAnAsn1ObjectIdentifier[]; + Version?: number; +}; +export type JsonWebKey = { + /** Key algorithm, parsed from `alg` header. */ + Algorithm?: string; + /** X.509 certificate thumbprint (SHA-1), parsed from `x5t` header. */ + CertificateThumbprintSHA1?: number[]; + /** X.509 certificate thumbprint (SHA-256), parsed from `x5t#S256` header. */ + CertificateThumbprintSHA256?: number[]; + /** X.509 certificate chain, parsed from `x5c` header. */ + Certificates?: ACertificateRepresentsAnX509Certificate[]; + CertificatesURL?: AUrlRepresentsAParsedUrlTechnicallyAUriReference; + /** Key is the Go in-memory representation of this key. It must have one + of these types: + ed25519.PublicKey + ed25519.PrivateKey + ecdsa.PublicKey + ecdsa.PrivateKey + rsa.PublicKey + rsa.PrivateKey + []byte (a symmetric key) + + When marshaling this JSONWebKey into JSON, the "kty" header parameter + will be automatically set based on the type of this field. */ + Key?: any; + /** Key identifier, parsed from `kid` header. */ + KeyID?: string; + /** Key use, parsed from `use` header. */ + Use?: string; +}; +export type Unstructured = { + /** Object is a JSON compatible map with string, float, int, bool, []any, + or map[string]any children. */ + Object?: { + [key: string]: any; + }; +}; +export type CreateDashboardSnapshotCommand = { + /** APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + +optional */ + apiVersion?: string; + dashboard: Unstructured; + /** Unique key used to delete the snapshot. It is different from the `key` so that only the creator can delete the snapshot. Required if `external` is `true`. */ + deleteKey?: string; + /** When the snapshot should expire in seconds in seconds. Default is never to expire. */ + expires?: number; + /** these are passed when storing an external snapshot ref + Save the snapshot on an external server rather than locally. */ + external?: boolean; + /** Define the unique key. Required if `external` is `true`. */ + key?: string; + /** Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + +optional */ + kind?: string; + /** Snapshot name */ + name?: string; +}; +export type CreateTeamCommand = { + email?: string; + name: string; +}; +export type TeamDto = { + accessControl?: { + [key: string]: boolean; + }; + avatarUrl?: string; + email?: string; + externalUID?: string; + /** @deprecated Use UID instead */ + id: number; + isProvisioned: boolean; + memberCount: number; + name: string; + orgId: number; + permission?: PermissionType; + uid: string; +}; +export type SearchTeamQueryResult = { + page?: number; + perPage?: number; + teams?: TeamDto[]; + totalCount?: number; +}; +export type TeamGroupDto = { + groupId?: string; + orgId?: number; + teamId?: number; +}; +export type TeamGroupMapping = { + groupId?: string; +}; +export type SearchTeamGroupsQueryResult = { + page?: number; + perPage?: number; + teamGroups?: TeamGroupDto[]; + totalCount?: number; +}; +export type UpdateTeamCommand = { + Email?: string; + ExternalUID?: string; + ID?: number; + Name?: string; +}; +export type TeamMemberDto = { + auth_module?: string; + avatarUrl?: string; + email?: string; + labels?: string[]; + login?: string; + name?: string; + orgId?: number; + permission?: PermissionType; + teamId?: number; + teamUID?: string; + uid?: string; + userId?: number; + userUID?: string; +}; +export type AddTeamMemberCommand = { + userId: number; +}; +export type SetTeamMembershipsCommand = { + admins?: string[]; + members?: string[]; +}; +export type UpdateTeamMemberCommand = { + permission?: PermissionType; +}; +export type UserProfileDto = { + accessControl?: { + [key: string]: boolean; + }; + authLabels?: string[]; + avatarUrl?: string; + createdAt?: string; + email?: string; + id?: number; + isDisabled?: boolean; + isExternal?: boolean; + isExternallySynced?: boolean; + isGrafanaAdmin?: boolean; + isGrafanaAdminExternallySynced?: boolean; + isProvisioned?: boolean; + login?: string; + name?: string; + orgId?: number; + theme?: string; + uid?: string; + updatedAt?: string; +}; +export type UpdateUserCommand = { + email?: string; + login?: string; + name?: string; + theme?: string; +}; +export type UserOrgDto = { + name?: string; + orgId?: number; + role?: 'None' | 'Viewer' | 'Editor' | 'Admin'; +}; +export type ChangeUserPasswordCommand = { + newPassword?: Password; + oldPassword?: Password; +}; +export type UserSearchHitDto = { + authLabels?: string[]; + avatarUrl?: string; + email?: string; + id?: number; + isAdmin?: boolean; + isDisabled?: boolean; + isProvisioned?: boolean; + lastSeenAt?: string; + lastSeenAtAge?: string; + login?: string; + name?: string; + uid?: string; +}; +export type SearchUserQueryResult = { + page?: number; + perPage?: number; + totalCount?: number; + users?: UserSearchHitDto[]; +}; +export type RelativeTimeRange = { + from?: Duration; + to?: Duration; +}; +export type AlertQueryRepresentsASingleQueryAssociatedWithAnAlertDefinition = { + /** Grafana data source unique identifier; it should be '__expr__' for a Server Side Expression operation. */ + datasourceUid?: string; + /** JSON is the raw JSON query and includes the above properties as well as custom properties. */ + model?: object; + /** QueryType is an optional identifier for the type of query. + It can be used to distinguish different types of queries. */ + queryType?: string; + /** RefID is the unique identifier of the query, set by the frontend call. */ + refId?: string; + relativeTimeRange?: RelativeTimeRange; +}; +export type AlertRuleNotificationSettings = { + /** Override the times when notifications should not be muted. These must match the name of a mute time interval defined + in the alertmanager configuration time_intervals section. All notifications will be suppressed unless they are sent + at the time that matches any interval. */ + active_time_intervals?: string[]; + /** Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for + cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels + use the special value '...' as the sole label name. + This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what + you want, unless you have a very low alert volume or your upstream notification system performs its own grouping. + Must include 'alertname' and 'grafana_folder' if not using '...'. */ + group_by?: string[]; + /** Override how long to wait before sending a notification about new alerts that are added to a group of alerts for + which an initial notification has already been sent. (Usually ~5m or more.) */ + group_interval?: string; + /** Override how long to initially wait to send a notification for a group of alerts. Allows to wait for an + inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.) */ + group_wait?: string; + /** Override the times when notifications should be muted. These must match the name of a mute time interval defined + in the alertmanager configuration time_intervals section. When muted it will not send any notifications, but + otherwise acts normally. */ + mute_time_intervals?: string[]; + /** Name of the receiver to send notifications to. */ + receiver: string; + /** Override how long to wait before sending a notification again if it has already been sent successfully for an + alert. (Usually ~3h or more). + Note that this parameter is implicitly bound by Alertmanager's `--data.retention` configuration flag. + Notifications will be resent after either repeat_interval or the data retention period have passed, whichever + occurs first. `repeat_interval` should not be less than `group_interval`. */ + repeat_interval?: string; +}; +export type Provenance = string; +export type Record = { + /** Which expression node should be used as the input for the recorded metric. */ + from: string; + /** Name of the recorded metric. */ + metric: string; + /** Which data source should be used to write the output of the recording rule, specified by UID. */ + target_datasource_uid?: string; +}; +export type ProvisionedAlertRule = { + annotations?: { + [key: string]: string; + }; + condition: string; + data: AlertQueryRepresentsASingleQueryAssociatedWithAnAlertDefinition[]; + execErrState: 'OK' | 'Alerting' | 'Error'; + folderUID: string; + for: string; + id?: number; + isPaused?: boolean; + keep_firing_for?: string; + labels?: { + [key: string]: string; + }; + missingSeriesEvalsToResolve?: number; + noDataState: 'Alerting' | 'NoData' | 'OK'; + notification_settings?: AlertRuleNotificationSettings; + orgID: number; + provenance?: Provenance; + record?: Record; + ruleGroup: string; + title: string; + uid?: string; +}; +export type ProvisionedAlertRuleRead = { + annotations?: { + [key: string]: string; + }; + condition: string; + data: AlertQueryRepresentsASingleQueryAssociatedWithAnAlertDefinition[]; + execErrState: 'OK' | 'Alerting' | 'Error'; + folderUID: string; + for: string; + id?: number; + isPaused?: boolean; + keep_firing_for?: string; + labels?: { + [key: string]: string; + }; + missingSeriesEvalsToResolve?: number; + noDataState: 'Alerting' | 'NoData' | 'OK'; + notification_settings?: AlertRuleNotificationSettings; + orgID: number; + provenance?: Provenance; + record?: Record; + ruleGroup: string; + title: string; + uid?: string; + updated?: string; +}; +export type ProvisionedAlertRules = ProvisionedAlertRule[]; +export type ProvisionedAlertRulesRead = ProvisionedAlertRuleRead[]; +export type ValidationError = { + message?: string; +}; +export type RawMessage = object; +export type ReceiverExportIsTheProvisionedFileExportOfAlertingReceiverV1 = { + disableResolveMessage?: boolean; + settings?: RawMessage; + type?: string; + uid?: string; +}; +export type ContactPointExportIsTheProvisionedFileExportOfAlertingContactPointV1 = { + name?: string; + orgId?: number; + receivers?: ReceiverExportIsTheProvisionedFileExportOfAlertingReceiverV1[]; +}; +export type RelativeTimeRangeExport = { + from?: number; + to?: number; +}; +export type AlertQueryExportIsTheProvisionedExportOfModelsAlertQuery = { + datasourceUid?: string; + model?: { + [key: string]: any; + }; + queryType?: string; + refId?: string; + relativeTimeRange?: RelativeTimeRangeExport; +}; +export type AlertRuleNotificationSettingsExportIsTheProvisionedExportOfModelsNotificationSettings = { + active_time_intervals?: string[]; + group_by?: string[]; + group_interval?: string; + group_wait?: string; + mute_time_intervals?: string[]; + receiver?: string; + repeat_interval?: string; +}; +export type RecordIsTheProvisionedExportOfModelsRecord = { + from?: string; + metric?: string; + targetDatasourceUid?: string; +}; +export type AlertRuleExportIsTheProvisionedFileExportOfModelsAlertRule = { + annotations?: { + [key: string]: string; + }; + condition?: string; + dashboardUid?: string; + data?: AlertQueryExportIsTheProvisionedExportOfModelsAlertQuery[]; + execErrState?: 'OK' | 'Alerting' | 'Error'; + for?: Duration; + isPaused?: boolean; + keepFiringFor?: Duration; + labels?: { + [key: string]: string; + }; + missing_series_evals_to_resolve?: number; + noDataState?: 'Alerting' | 'NoData' | 'OK'; + notification_settings?: AlertRuleNotificationSettingsExportIsTheProvisionedExportOfModelsNotificationSettings; + panelId?: number; + record?: RecordIsTheProvisionedExportOfModelsRecord; + title?: string; + uid?: string; +}; +export type AlertRuleGroupExportIsTheProvisionedFileExportOfAlertRuleGroupV1 = { + folder?: string; + interval?: Duration; + name?: string; + orgId?: number; + rules?: AlertRuleExportIsTheProvisionedFileExportOfModelsAlertRule[]; +}; +export type TimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted = { + name?: string; + time_intervals?: TimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted[]; +}; +export type MuteTimeIntervalExport = { + name?: string; + orgId?: number; + time_intervals?: TimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted[]; +}; +export type MatchRegexpsRepresentsAMapOfRegexp = { + [key: string]: string; +}; +export type MatchTypeIsAnEnumForLabelMatchingTypes = number; +export type MatcherModelsTheMatchingOfALabel = { + Name?: string; + Type?: MatchTypeIsAnEnumForLabelMatchingTypes; + Value?: string; +}; +export type Matchers = MatcherModelsTheMatchingOfALabel[]; +export type ObjectMatcherIsAMatcherThatCanBeUsedToFilterAlerts = string[]; +export type ObjectMatchersIsAListOfMatchersThatCanBeUsedToFilterAlerts = + ObjectMatcherIsAMatcherThatCanBeUsedToFilterAlerts[]; +export type RouteExport = { + active_time_intervals?: string[]; + continue?: boolean; + group_by?: string[]; + group_interval?: string; + group_wait?: string; + /** Deprecated. Remove before v1.0 release. */ + match?: { + [key: string]: string; + }; + match_re?: MatchRegexpsRepresentsAMapOfRegexp; + matchers?: Matchers; + mute_time_intervals?: string[]; + object_matchers?: ObjectMatchersIsAListOfMatchersThatCanBeUsedToFilterAlerts; + receiver?: string; + repeat_interval?: string; + routes?: RouteExport[]; +}; +export type NotificationPolicyExportIsTheProvisionedFileExportOfAlertingNotificiationPolicyV1 = { + active_time_intervals?: string[]; + continue?: boolean; + group_by?: string[]; + group_interval?: string; + group_wait?: string; + /** Deprecated. Remove before v1.0 release. */ + match?: { + [key: string]: string; + }; + match_re?: MatchRegexpsRepresentsAMapOfRegexp; + matchers?: Matchers; + mute_time_intervals?: string[]; + object_matchers?: ObjectMatchersIsAListOfMatchersThatCanBeUsedToFilterAlerts; + orgId?: number; + receiver?: string; + repeat_interval?: string; + routes?: RouteExport[]; +}; +export type AlertingFileExportIsTheFullProvisionedFileExport = { + apiVersion?: number; + contactPoints?: ContactPointExportIsTheProvisionedFileExportOfAlertingContactPointV1[]; + groups?: AlertRuleGroupExportIsTheProvisionedFileExportOfAlertRuleGroupV1[]; + muteTimes?: MuteTimeIntervalExport[]; + policies?: NotificationPolicyExportIsTheProvisionedFileExportOfAlertingNotificiationPolicyV1[]; +}; +export type EmbeddedContactPoint = { + disableResolveMessage?: boolean; + /** Name is used as grouping key in the UI. Contact points with the + same name will be grouped in the UI. */ + name?: string; + settings: Json; + type: + | 'alertmanager' + | 'dingding' + | 'discord' + | 'email' + | 'googlechat' + | 'kafka' + | 'line' + | 'opsgenie' + | 'pagerduty' + | 'pushover' + | 'sensugo' + | 'slack' + | 'teams' + | 'telegram' + | 'threema' + | 'victorops' + | 'webhook' + | 'wecom'; + /** UID is the unique identifier of the contact point. The UID can be + set by the user. */ + uid?: string; +}; +export type EmbeddedContactPointRead = { + disableResolveMessage?: boolean; + /** Name is used as grouping key in the UI. Contact points with the + same name will be grouped in the UI. */ + name?: string; + provenance?: string; + settings: Json; + type: + | 'alertmanager' + | 'dingding' + | 'discord' + | 'email' + | 'googlechat' + | 'kafka' + | 'line' + | 'opsgenie' + | 'pagerduty' + | 'pushover' + | 'sensugo' + | 'slack' + | 'teams' + | 'telegram' + | 'threema' + | 'victorops' + | 'webhook' + | 'wecom'; + /** UID is the unique identifier of the contact point. The UID can be + set by the user. */ + uid?: string; +}; +export type ContactPoints = EmbeddedContactPoint[]; +export type ContactPointsRead = EmbeddedContactPointRead[]; +export type PermissionDenied = object; +export type Ack = object; +export type NotFound = object; +export type AlertRuleGroup = { + folderUid?: string; + interval?: number; + rules?: ProvisionedAlertRule[]; + title?: string; +}; +export type AlertRuleGroupRead = { + folderUid?: string; + interval?: number; + rules?: ProvisionedAlertRuleRead[]; + title?: string; +}; +export type MuteTimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted = { + name?: string; + time_intervals?: TimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted[]; +}; +export type MuteTimings = MuteTimeIntervalRepresentsANamedSetOfTimeIntervalsForWhichARouteShouldBeMuted[]; +export type Route = { + active_time_intervals?: string[]; + continue?: boolean; + group_by?: string[]; + group_interval?: string; + group_wait?: string; + /** Deprecated. Remove before v1.0 release. */ + match?: { + [key: string]: string; + }; + match_re?: MatchRegexpsRepresentsAMapOfRegexp; + matchers?: Matchers; + mute_time_intervals?: string[]; + object_matchers?: ObjectMatchersIsAListOfMatchersThatCanBeUsedToFilterAlerts; + provenance?: Provenance; + receiver?: string; + repeat_interval?: string; + routes?: Route[]; +}; +export type NotificationTemplate = { + name?: string; + provenance?: Provenance; + template?: string; + version?: string; +}; +export type NotificationTemplates = NotificationTemplate[]; +export type NotificationTemplateContent = { + template?: string; + version?: string; +}; +export const { + useSearchResultMutation, + useListRolesQuery, + useLazyListRolesQuery, + useCreateRoleMutation, + useDeleteRoleMutation, + useGetRoleQuery, + useLazyGetRoleQuery, + useUpdateRoleMutation, + useGetRoleAssignmentsQuery, + useLazyGetRoleAssignmentsQuery, + useSetRoleAssignmentsMutation, + useGetAccessControlStatusQuery, + useLazyGetAccessControlStatusQuery, + useListTeamsRolesQuery, + useLazyListTeamsRolesQuery, + useListTeamRolesQuery, + useLazyListTeamRolesQuery, + useAddTeamRoleMutation, + useSetTeamRolesMutation, + useRemoveTeamRoleMutation, + useListUsersRolesMutation, + useListUserRolesQuery, + useLazyListUserRolesQuery, + useAddUserRoleMutation, + useSetUserRolesMutation, + useRemoveUserRoleMutation, + useGetResourceDescriptionQuery, + useLazyGetResourceDescriptionQuery, + useGetResourcePermissionsQuery, + useLazyGetResourcePermissionsQuery, + useSetResourcePermissionsMutation, + useSetResourcePermissionsForBuiltInRoleMutation, + useSetResourcePermissionsForTeamMutation, + useSetResourcePermissionsForUserMutation, + useGetSyncStatusQuery, + useLazyGetSyncStatusQuery, + useReloadLdapCfgMutation, + useGetLdapStatusQuery, + useLazyGetLdapStatusQuery, + usePostSyncUserWithLdapMutation, + useGetUserFromLdapQuery, + useLazyGetUserFromLdapQuery, + useAdminProvisioningReloadAccessControlMutation, + useAdminProvisioningReloadDashboardsMutation, + useAdminProvisioningReloadDatasourcesMutation, + useAdminProvisioningReloadPluginsMutation, + useAdminGetSettingsQuery, + useLazyAdminGetSettingsQuery, + useAdminGetStatsQuery, + useLazyAdminGetStatsQuery, + useAdminCreateUserMutation, + useAdminDeleteUserMutation, + useAdminGetUserAuthTokensQuery, + useLazyAdminGetUserAuthTokensQuery, + useAdminDisableUserMutation, + useAdminEnableUserMutation, + useAdminLogoutUserMutation, + useAdminUpdateUserPasswordMutation, + useAdminUpdateUserPermissionsMutation, + useGetUserQuotaQuery, + useLazyGetUserQuotaQuery, + useUpdateUserQuotaMutation, + useAdminRevokeUserAuthTokenMutation, + useGetAnnotationsQuery, + useLazyGetAnnotationsQuery, + usePostAnnotationMutation, + usePostGraphiteAnnotationMutation, + useMassDeleteAnnotationsMutation, + useGetAnnotationTagsQuery, + useLazyGetAnnotationTagsQuery, + useDeleteAnnotationByIdMutation, + useGetAnnotationByIdQuery, + useLazyGetAnnotationByIdQuery, + usePatchAnnotationMutation, + useUpdateAnnotationMutation, + useListDevicesQuery, + useLazyListDevicesQuery, + useSearchDevicesQuery, + useLazySearchDevicesQuery, + useGetSessionListQuery, + useLazyGetSessionListQuery, + useCreateSessionMutation, + useDeleteSessionMutation, + useGetSessionQuery, + useLazyGetSessionQuery, + useCreateSnapshotMutation, + useGetSnapshotQuery, + useLazyGetSnapshotQuery, + useCancelSnapshotMutation, + useUploadSnapshotMutation, + useGetShapshotListQuery, + useLazyGetShapshotListQuery, + useGetResourceDependenciesQuery, + useLazyGetResourceDependenciesQuery, + useGetCloudMigrationTokenQuery, + useLazyGetCloudMigrationTokenQuery, + useCreateCloudMigrationTokenMutation, + useDeleteCloudMigrationTokenMutation, + useRouteConvertPrometheusCortexGetRulesQuery, + useLazyRouteConvertPrometheusCortexGetRulesQuery, + useRouteConvertPrometheusCortexPostRuleGroupsMutation, + useRouteConvertPrometheusCortexDeleteNamespaceMutation, + useRouteConvertPrometheusCortexGetNamespaceQuery, + useLazyRouteConvertPrometheusCortexGetNamespaceQuery, + useRouteConvertPrometheusCortexPostRuleGroupMutation, + useRouteConvertPrometheusCortexDeleteRuleGroupMutation, + useRouteConvertPrometheusCortexGetRuleGroupQuery, + useLazyRouteConvertPrometheusCortexGetRuleGroupQuery, + useRouteConvertPrometheusGetRulesQuery, + useLazyRouteConvertPrometheusGetRulesQuery, + useRouteConvertPrometheusPostRuleGroupsMutation, + useRouteConvertPrometheusDeleteNamespaceMutation, + useRouteConvertPrometheusGetNamespaceQuery, + useLazyRouteConvertPrometheusGetNamespaceQuery, + useRouteConvertPrometheusPostRuleGroupMutation, + useRouteConvertPrometheusDeleteRuleGroupMutation, + useRouteConvertPrometheusGetRuleGroupQuery, + useLazyRouteConvertPrometheusGetRuleGroupQuery, + useSearchDashboardSnapshotsQuery, + useLazySearchDashboardSnapshotsQuery, + useCalculateDashboardDiffMutation, + usePostDashboardMutation, + useGetHomeDashboardQuery, + useLazyGetHomeDashboardQuery, + useImportDashboardMutation, + useInterpolateDashboardMutation, + useListPublicDashboardsQuery, + useLazyListPublicDashboardsQuery, + useGetDashboardTagsQuery, + useLazyGetDashboardTagsQuery, + useGetPublicDashboardQuery, + useLazyGetPublicDashboardQuery, + useCreatePublicDashboardMutation, + useDeletePublicDashboardMutation, + useUpdatePublicDashboardMutation, + useDeleteDashboardByUidMutation, + useGetDashboardByUidQuery, + useLazyGetDashboardByUidQuery, + useGetDashboardPermissionsListByUidQuery, + useLazyGetDashboardPermissionsListByUidQuery, + useUpdateDashboardPermissionsByUidMutation, + useRestoreDashboardVersionByUidMutation, + useGetDashboardVersionsByUidQuery, + useLazyGetDashboardVersionsByUidQuery, + useGetDashboardVersionByUidQuery, + useLazyGetDashboardVersionByUidQuery, + useGetDataSourcesQuery, + useLazyGetDataSourcesQuery, + useAddDataSourceMutation, + useGetCorrelationsQuery, + useLazyGetCorrelationsQuery, + useGetDataSourceIdByNameQuery, + useLazyGetDataSourceIdByNameQuery, + useDeleteDataSourceByNameMutation, + useGetDataSourceByNameQuery, + useLazyGetDataSourceByNameQuery, + useDatasourceProxyDeleteByUiDcallsMutation, + useDatasourceProxyGetByUiDcallsQuery, + useLazyDatasourceProxyGetByUiDcallsQuery, + useDatasourceProxyPostByUiDcallsMutation, + useGetCorrelationsBySourceUidQuery, + useLazyGetCorrelationsBySourceUidQuery, + useCreateCorrelationMutation, + useGetCorrelationQuery, + useLazyGetCorrelationQuery, + useUpdateCorrelationMutation, + useDeleteDataSourceByUidMutation, + useGetDataSourceByUidQuery, + useLazyGetDataSourceByUidQuery, + useUpdateDataSourceByUidMutation, + useDeleteCorrelationMutation, + useCheckDatasourceHealthWithUidQuery, + useLazyCheckDatasourceHealthWithUidQuery, + useGetTeamLbacRulesApiQuery, + useLazyGetTeamLbacRulesApiQuery, + useUpdateTeamLbacRulesApiMutation, + useCallDatasourceResourceWithUidQuery, + useLazyCallDatasourceResourceWithUidQuery, + useGetDataSourceCacheConfigQuery, + useLazyGetDataSourceCacheConfigQuery, + useSetDataSourceCacheConfigMutation, + useCleanDataSourceCacheMutation, + useDisableDataSourceCacheMutation, + useEnableDataSourceCacheMutation, + useQueryMetricsWithExpressionsMutation, + useGetFoldersQuery, + useLazyGetFoldersQuery, + useCreateFolderMutation, + useDeleteFolderMutation, + useGetFolderByUidQuery, + useLazyGetFolderByUidQuery, + useUpdateFolderMutation, + useGetFolderDescendantCountsQuery, + useLazyGetFolderDescendantCountsQuery, + useMoveFolderMutation, + useGetFolderPermissionListQuery, + useLazyGetFolderPermissionListQuery, + useUpdateFolderPermissionsMutation, + useGetMappedGroupsQuery, + useLazyGetMappedGroupsQuery, + useDeleteGroupMappingsMutation, + useCreateGroupMappingsMutation, + useUpdateGroupMappingsMutation, + useGetGroupRolesQuery, + useLazyGetGroupRolesQuery, + useGetHealthQuery, + useLazyGetHealthQuery, + useGetLibraryElementsQuery, + useLazyGetLibraryElementsQuery, + useCreateLibraryElementMutation, + useGetLibraryElementByNameQuery, + useLazyGetLibraryElementByNameQuery, + useDeleteLibraryElementByUidMutation, + useGetLibraryElementByUidQuery, + useLazyGetLibraryElementByUidQuery, + useUpdateLibraryElementMutation, + useGetLibraryElementConnectionsQuery, + useLazyGetLibraryElementConnectionsQuery, + useGetStatusQuery, + useLazyGetStatusQuery, + useRefreshLicenseStatsQuery, + useLazyRefreshLicenseStatsQuery, + useDeleteLicenseTokenMutation, + useGetLicenseTokenQuery, + useLazyGetLicenseTokenQuery, + usePostLicenseTokenMutation, + usePostRenewLicenseTokenMutation, + useGetSamlLogoutQuery, + useLazyGetSamlLogoutQuery, + useGetCurrentOrgQuery, + useLazyGetCurrentOrgQuery, + useUpdateCurrentOrgMutation, + useUpdateCurrentOrgAddressMutation, + useGetPendingOrgInvitesQuery, + useLazyGetPendingOrgInvitesQuery, + useAddOrgInviteMutation, + useRevokeInviteMutation, + useGetOrgPreferencesQuery, + useLazyGetOrgPreferencesQuery, + usePatchOrgPreferencesMutation, + useUpdateOrgPreferencesMutation, + useGetCurrentOrgQuotaQuery, + useLazyGetCurrentOrgQuotaQuery, + useGetOrgUsersForCurrentOrgQuery, + useLazyGetOrgUsersForCurrentOrgQuery, + useAddOrgUserToCurrentOrgMutation, + useGetOrgUsersForCurrentOrgLookupQuery, + useLazyGetOrgUsersForCurrentOrgLookupQuery, + useRemoveOrgUserForCurrentOrgMutation, + useUpdateOrgUserForCurrentOrgMutation, + useSearchOrgsQuery, + useLazySearchOrgsQuery, + useCreateOrgMutation, + useGetOrgByNameQuery, + useLazyGetOrgByNameQuery, + useDeleteOrgByIdMutation, + useGetOrgByIdQuery, + useLazyGetOrgByIdQuery, + useUpdateOrgMutation, + useUpdateOrgAddressMutation, + useGetOrgQuotaQuery, + useLazyGetOrgQuotaQuery, + useUpdateOrgQuotaMutation, + useGetOrgUsersQuery, + useLazyGetOrgUsersQuery, + useAddOrgUserMutation, + useSearchOrgUsersQuery, + useLazySearchOrgUsersQuery, + useRemoveOrgUserMutation, + useUpdateOrgUserMutation, + useSearchPlaylistsQuery, + useLazySearchPlaylistsQuery, + useCreatePlaylistMutation, + useDeletePlaylistMutation, + useGetPlaylistQuery, + useLazyGetPlaylistQuery, + useUpdatePlaylistMutation, + useGetPlaylistItemsQuery, + useLazyGetPlaylistItemsQuery, + useViewPublicDashboardQuery, + useLazyViewPublicDashboardQuery, + useGetPublicAnnotationsQuery, + useLazyGetPublicAnnotationsQuery, + useQueryPublicDashboardMutation, + useSearchQueriesQuery, + useLazySearchQueriesQuery, + useCreateQueryMutation, + useUnstarQueryMutation, + useStarQueryMutation, + useDeleteQueryMutation, + usePatchQueryCommentMutation, + useListRecordingRulesQuery, + useLazyListRecordingRulesQuery, + useCreateRecordingRuleMutation, + useUpdateRecordingRuleMutation, + useTestCreateRecordingRuleMutation, + useDeleteRecordingRuleWriteTargetMutation, + useGetRecordingRuleWriteTargetQuery, + useLazyGetRecordingRuleWriteTargetQuery, + useCreateRecordingRuleWriteTargetMutation, + useDeleteRecordingRuleMutation, + useGetReportsQuery, + useLazyGetReportsQuery, + useCreateReportMutation, + useGetReportsByDashboardUidQuery, + useLazyGetReportsByDashboardUidQuery, + useSendReportMutation, + useGetSettingsImageQuery, + useLazyGetSettingsImageQuery, + useRenderReportCsVsQuery, + useLazyRenderReportCsVsQuery, + useRenderReportPdFsQuery, + useLazyRenderReportPdFsQuery, + useGetReportSettingsQuery, + useLazyGetReportSettingsQuery, + useSaveReportSettingsMutation, + useSendTestEmailMutation, + usePostAcsMutation, + useGetMetadataQuery, + useLazyGetMetadataQuery, + useGetSloQuery, + useLazyGetSloQuery, + usePostSloMutation, + useSearchQuery, + useLazySearchQuery, + useListSortOptionsQuery, + useLazyListSortOptionsQuery, + useCreateServiceAccountMutation, + useSearchOrgServiceAccountsWithPagingQuery, + useLazySearchOrgServiceAccountsWithPagingQuery, + useDeleteServiceAccountMutation, + useRetrieveServiceAccountQuery, + useLazyRetrieveServiceAccountQuery, + useUpdateServiceAccountMutation, + useListTokensQuery, + useLazyListTokensQuery, + useCreateTokenMutation, + useDeleteTokenMutation, + useRetrieveJwksQuery, + useLazyRetrieveJwksQuery, + useGetSharingOptionsQuery, + useLazyGetSharingOptionsQuery, + useCreateDashboardSnapshotMutation, + useDeleteDashboardSnapshotByDeleteKeyQuery, + useLazyDeleteDashboardSnapshotByDeleteKeyQuery, + useDeleteDashboardSnapshotMutation, + useGetDashboardSnapshotQuery, + useLazyGetDashboardSnapshotQuery, + useCreateTeamMutation, + useSearchTeamsQuery, + useLazySearchTeamsQuery, + useRemoveTeamGroupApiQueryMutation, + useGetTeamGroupsApiQuery, + useLazyGetTeamGroupsApiQuery, + useAddTeamGroupApiMutation, + useSearchTeamGroupsQuery, + useLazySearchTeamGroupsQuery, + useDeleteTeamByIdMutation, + useGetTeamByIdQuery, + useLazyGetTeamByIdQuery, + useUpdateTeamMutation, + useGetTeamMembersQuery, + useLazyGetTeamMembersQuery, + useAddTeamMemberMutation, + useSetTeamMembershipsMutation, + useRemoveTeamMemberMutation, + useUpdateTeamMemberMutation, + useGetTeamPreferencesQuery, + useLazyGetTeamPreferencesQuery, + useUpdateTeamPreferencesMutation, + useGetSignedInUserQuery, + useLazyGetSignedInUserQuery, + useUpdateSignedInUserMutation, + useGetUserAuthTokensQuery, + useLazyGetUserAuthTokensQuery, + useUpdateUserEmailQuery, + useLazyUpdateUserEmailQuery, + useClearHelpFlagsQuery, + useLazyClearHelpFlagsQuery, + useSetHelpFlagMutation, + useGetSignedInUserOrgListQuery, + useLazyGetSignedInUserOrgListQuery, + useChangeUserPasswordMutation, + useGetUserPreferencesQuery, + useLazyGetUserPreferencesQuery, + usePatchUserPreferencesMutation, + useUpdateUserPreferencesMutation, + useGetUserQuotasQuery, + useLazyGetUserQuotasQuery, + useRevokeUserAuthTokenMutation, + useUnstarDashboardByUidMutation, + useStarDashboardByUidMutation, + useGetSignedInUserTeamListQuery, + useLazyGetSignedInUserTeamListQuery, + useUserSetUsingOrgMutation, + useSearchUsersQuery, + useLazySearchUsersQuery, + useGetUserByLoginOrEmailQuery, + useLazyGetUserByLoginOrEmailQuery, + useSearchUsersWithPagingQuery, + useLazySearchUsersWithPagingQuery, + useGetUserByIdQuery, + useLazyGetUserByIdQuery, + useUpdateUserMutation, + useGetUserOrgListQuery, + useLazyGetUserOrgListQuery, + useGetUserTeamsQuery, + useLazyGetUserTeamsQuery, + useRouteGetAlertRulesQuery, + useLazyRouteGetAlertRulesQuery, + useRoutePostAlertRuleMutation, + useRouteGetAlertRulesExportQuery, + useLazyRouteGetAlertRulesExportQuery, + useRouteDeleteAlertRuleMutation, + useRouteGetAlertRuleQuery, + useLazyRouteGetAlertRuleQuery, + useRoutePutAlertRuleMutation, + useRouteGetAlertRuleExportQuery, + useLazyRouteGetAlertRuleExportQuery, + useRouteGetContactpointsQuery, + useLazyRouteGetContactpointsQuery, + useRoutePostContactpointsMutation, + useRouteGetContactpointsExportQuery, + useLazyRouteGetContactpointsExportQuery, + useRouteDeleteContactpointsMutation, + useRoutePutContactpointMutation, + useRouteDeleteAlertRuleGroupMutation, + useRouteGetAlertRuleGroupQuery, + useLazyRouteGetAlertRuleGroupQuery, + useRoutePutAlertRuleGroupMutation, + useRouteGetAlertRuleGroupExportQuery, + useLazyRouteGetAlertRuleGroupExportQuery, + useRouteGetMuteTimingsQuery, + useLazyRouteGetMuteTimingsQuery, + useRoutePostMuteTimingMutation, + useRouteExportMuteTimingsQuery, + useLazyRouteExportMuteTimingsQuery, + useRouteDeleteMuteTimingMutation, + useRouteGetMuteTimingQuery, + useLazyRouteGetMuteTimingQuery, + useRoutePutMuteTimingMutation, + useRouteExportMuteTimingQuery, + useLazyRouteExportMuteTimingQuery, + useRouteResetPolicyTreeMutation, + useRouteGetPolicyTreeQuery, + useLazyRouteGetPolicyTreeQuery, + useRoutePutPolicyTreeMutation, + useRouteGetPolicyTreeExportQuery, + useLazyRouteGetPolicyTreeExportQuery, + useRouteGetTemplatesQuery, + useLazyRouteGetTemplatesQuery, + useRouteDeleteTemplateMutation, + useRouteGetTemplateQuery, + useLazyRouteGetTemplateQuery, + useRoutePutTemplateMutation, + useListAllProvidersSettingsQuery, + useLazyListAllProvidersSettingsQuery, + useRemoveProviderSettingsMutation, + useGetProviderSettingsQuery, + useLazyGetProviderSettingsQuery, + useUpdateProviderSettingsMutation, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/index.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/index.ts new file mode 100644 index 00000000000..722532483b6 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/index.ts @@ -0,0 +1,5 @@ +import { generatedAPI as rawAPI } from './endpoints.gen'; + +export const legacyAPI = rawAPI.enhanceEndpoints({}); + +export * from './endpoints.gen'; diff --git a/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/endpoints.gen.ts index 87bcb349ecd..d1aff7226ee 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/migrate-to-cloud/endpoints.gen.ts @@ -405,18 +405,26 @@ export type LibraryElementResponseIsAResponseStructForLibraryElementDto = { }; export const { useGetSessionListQuery, + useLazyGetSessionListQuery, useCreateSessionMutation, useDeleteSessionMutation, useGetSessionQuery, + useLazyGetSessionQuery, useCreateSnapshotMutation, useGetSnapshotQuery, + useLazyGetSnapshotQuery, useCancelSnapshotMutation, useUploadSnapshotMutation, useGetShapshotListQuery, + useLazyGetShapshotListQuery, useGetResourceDependenciesQuery, + useLazyGetResourceDependenciesQuery, useGetCloudMigrationTokenQuery, + useLazyGetCloudMigrationTokenQuery, useCreateCloudMigrationTokenMutation, useDeleteCloudMigrationTokenMutation, useGetDashboardByUidQuery, + useLazyGetDashboardByUidQuery, useGetLibraryElementByUidQuery, + useLazyGetLibraryElementByUidQuery, } = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/endpoints.gen.ts index 7a42a89de07..3c7edccaf9c 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/playlist/v0alpha1/endpoints.gen.ts @@ -1,11 +1,15 @@ import { api } from './baseAPI'; -export const addTagTypes = ['Playlist'] as const; +export const addTagTypes = ['API Discovery', 'Playlist'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ + getApiResources: build.query({ + query: () => ({ url: `/` }), + providesTags: ['API Discovery'], + }), listPlaylist: build.query({ query: (queryArg) => ({ url: `/playlists`, @@ -39,6 +43,29 @@ const injectedRtkApi = api }), invalidatesTags: ['Playlist'], }), + deletecollectionPlaylist: build.mutation({ + query: (queryArg) => ({ + url: `/playlists`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['Playlist'], + }), getPlaylist: build.query({ query: (queryArg) => ({ url: `/playlists/${queryArg.name}`, @@ -77,10 +104,65 @@ const injectedRtkApi = api }), invalidatesTags: ['Playlist'], }), + updatePlaylist: build.mutation({ + query: (queryArg) => ({ + url: `/playlists/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Playlist'], + }), + getPlaylistStatus: build.query({ + query: (queryArg) => ({ + url: `/playlists/${queryArg.name}/status`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Playlist'], + }), + replacePlaylistStatus: build.mutation({ + query: (queryArg) => ({ + url: `/playlists/${queryArg.name}/status`, + method: 'PUT', + body: queryArg.playlist, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Playlist'], + }), + updatePlaylistStatus: build.mutation({ + query: (queryArg) => ({ + url: `/playlists/${queryArg.name}/status`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Playlist'], + }), }), overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; +export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; +export type GetApiResourcesApiArg = void; export type ListPlaylistApiResponse = /** status 200 OK */ PlaylistList; export type ListPlaylistApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -141,6 +223,57 @@ export type CreatePlaylistApiArg = { fieldValidation?: string; playlist: Playlist; }; +export type DeletecollectionPlaylistApiResponse = /** status 200 OK */ Status; +export type DeletecollectionPlaylistApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; export type GetPlaylistApiResponse = /** status 200 OK */ Playlist; export type GetPlaylistApiArg = { /** name of the Playlist */ @@ -179,6 +312,91 @@ export type DeletePlaylistApiArg = { /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ propagationPolicy?: string; }; +export type UpdatePlaylistApiResponse = /** status 200 OK */ Playlist | /** status 201 Created */ Playlist; +export type UpdatePlaylistApiArg = { + /** name of the Playlist */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type GetPlaylistStatusApiResponse = /** status 200 OK */ Playlist; +export type GetPlaylistStatusApiArg = { + /** name of the Playlist */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplacePlaylistStatusApiResponse = /** status 200 OK */ Playlist | /** status 201 Created */ Playlist; +export type ReplacePlaylistStatusApiArg = { + /** name of the Playlist */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + playlist: Playlist; +}; +export type UpdatePlaylistStatusApiResponse = /** status 200 OK */ Playlist | /** status 201 Created */ Playlist; +export type UpdatePlaylistStatusApiArg = { + /** name of the Playlist */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type ApiResource = { + /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ + categories?: string[]; + /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ + group?: string; + /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ + kind: string; + /** name is the plural name of the resource. */ + name: string; + /** namespaced indicates if a resource is namespaced or not. */ + namespaced: boolean; + /** shortNames is a list of suggested short names of the resource. */ + shortNames?: string[]; + /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ + singularName: string; + /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ + storageVersionHash?: string; + /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ + verbs: string[]; + /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ + version?: string; +}; +export type ApiResourceList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** groupVersion is the group and version this APIResourceList is for. */ + groupVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** resources contains the name of the resources and if they are namespaced. */ + resources: ApiResource[]; +}; export type Time = string; export type FieldsV1 = object; export type ManagedFieldsEntry = { @@ -377,3 +595,21 @@ export type Status = { /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */ status?: string; }; +export type Patch = object; +export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, + useListPlaylistQuery, + useLazyListPlaylistQuery, + useCreatePlaylistMutation, + useDeletecollectionPlaylistMutation, + useGetPlaylistQuery, + useLazyGetPlaylistQuery, + useReplacePlaylistMutation, + useDeletePlaylistMutation, + useUpdatePlaylistMutation, + useGetPlaylistStatusQuery, + useLazyGetPlaylistStatusQuery, + useReplacePlaylistStatusMutation, + useUpdatePlaylistStatusMutation, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/preferences/user/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/user/endpoints.gen.ts index 60162b33858..71107ce1072 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/preferences/user/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/preferences/user/endpoints.gen.ts @@ -102,5 +102,9 @@ export type UpdatePrefsCmd = { timezone?: 'utc' | 'browser'; weekStart?: string; }; -export const { useGetUserPreferencesQuery, usePatchUserPreferencesMutation, useUpdateUserPreferencesMutation } = - injectedRtkApi; +export const { + useGetUserPreferencesQuery, + useLazyGetUserPreferencesQuery, + usePatchUserPreferencesMutation, + useUpdateUserPreferencesMutation, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts index 2ea0fd00730..c90f694defb 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts @@ -7,7 +7,7 @@ const injectedRtkApi = api .injectEndpoints({ endpoints: (build) => ({ getApiResources: build.query({ - query: () => ({ url: `/apis/preferences.grafana.app/v1alpha1/` }), + query: () => ({ url: `/` }), providesTags: ['API Discovery'], }), listPreferences: build.query({ @@ -778,17 +778,23 @@ export type StarsList = { }; export const { useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, useListPreferencesQuery, + useLazyListPreferencesQuery, useCreatePreferencesMutation, useMergedPreferencesQuery, + useLazyMergedPreferencesQuery, useGetPreferencesQuery, + useLazyGetPreferencesQuery, useReplacePreferencesMutation, useDeletePreferencesMutation, useUpdatePreferencesMutation, useListStarsQuery, + useLazyListStarsQuery, useCreateStarsMutation, useDeletecollectionStarsMutation, useGetStarsQuery, + useLazyGetStarsQuery, useReplaceStarsMutation, useDeleteStarsMutation, useUpdateStarsMutation, diff --git a/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts index 41e4cac3c01..67bdca12d27 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/provisioning/v0alpha1/endpoints.gen.ts @@ -1,11 +1,15 @@ import { api } from './baseAPI'; -export const addTagTypes = ['Job', 'Repository', 'Provisioning'] as const; +export const addTagTypes = ['API Discovery', 'Job', 'Repository', 'Provisioning'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ + getApiResources: build.query({ + query: () => ({ url: `/` }), + providesTags: ['API Discovery'], + }), listJob: build.query({ query: (queryArg) => ({ url: `/jobs`, @@ -100,6 +104,21 @@ const injectedRtkApi = api }), invalidatesTags: ['Job'], }), + updateJob: build.mutation({ + query: (queryArg) => ({ + url: `/jobs/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Job'], + }), listRepository: build.query({ query: (queryArg) => ({ url: `/repositories`, @@ -197,6 +216,21 @@ const injectedRtkApi = api }), invalidatesTags: ['Repository'], }), + updateRepository: build.mutation({ + query: (queryArg) => ({ + url: `/repositories/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Repository'], + }), getRepositoryFiles: build.query({ query: (queryArg) => ({ url: `/repositories/${queryArg.name}/files/`, @@ -336,6 +370,21 @@ const injectedRtkApi = api }), invalidatesTags: ['Repository'], }), + updateRepositoryStatus: build.mutation({ + query: (queryArg) => ({ + url: `/repositories/${queryArg.name}/status`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Repository'], + }), createRepositoryTest: build.mutation({ query: (queryArg) => ({ url: `/repositories/${queryArg.name}/test`, method: 'POST', body: queryArg.body }), invalidatesTags: ['Repository'], @@ -360,6 +409,8 @@ const injectedRtkApi = api overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; +export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; +export type GetApiResourcesApiArg = void; export type ListJobApiResponse = /** status 200 OK */ JobList; export type ListJobApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -509,6 +560,22 @@ export type DeleteJobApiArg = { /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ propagationPolicy?: string; }; +export type UpdateJobApiResponse = /** status 200 OK */ Job | /** status 201 Created */ Job; +export type UpdateJobApiArg = { + /** name of the Job */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; export type ListRepositoryApiResponse = /** status 200 OK */ RepositoryList; export type ListRepositoryApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -658,6 +725,22 @@ export type DeleteRepositoryApiArg = { /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ propagationPolicy?: string; }; +export type UpdateRepositoryApiResponse = /** status 200 OK */ Repository | /** status 201 Created */ Repository; +export type UpdateRepositoryApiArg = { + /** name of the Repository */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; export type GetRepositoryFilesApiResponse = /** status 200 OK */ { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; @@ -811,6 +894,22 @@ export type ReplaceRepositoryStatusApiArg = { fieldValidation?: string; repository: Repository; }; +export type UpdateRepositoryStatusApiResponse = /** status 200 OK */ Repository | /** status 201 Created */ Repository; +export type UpdateRepositoryStatusApiArg = { + /** name of the Repository */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; export type CreateRepositoryTestApiResponse = /** status 200 OK */ TestResults; export type CreateRepositoryTestApiArg = { /** name of the TestResults */ @@ -840,6 +939,38 @@ export type GetFrontendSettingsApiResponse = /** status 200 undefined */ Reposit export type GetFrontendSettingsApiArg = void; export type GetResourceStatsApiResponse = /** status 200 undefined */ ResourceStats; export type GetResourceStatsApiArg = void; +export type ApiResource = { + /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ + categories?: string[]; + /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ + group?: string; + /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ + kind: string; + /** name is the plural name of the resource. */ + name: string; + /** namespaced indicates if a resource is namespaced or not. */ + namespaced: boolean; + /** shortNames is a list of suggested short names of the resource. */ + shortNames?: string[]; + /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ + singularName: string; + /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ + storageVersionHash?: string; + /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ + verbs: string[]; + /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ + version?: string; +}; +export type ApiResourceList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** groupVersion is the group and version this APIResourceList is for. */ + groupVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** resources contains the name of the resources and if they are namespaced. */ + resources: ApiResource[]; +}; export type Time = string; export type FieldsV1 = object; export type ManagedFieldsEntry = { @@ -1117,6 +1248,7 @@ export type Status = { /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */ status?: string; }; +export type Patch = object; export type InlineSecureValue = | { /** Create a secure value -- this is only used for POST/PUT */ @@ -1506,36 +1638,58 @@ export type ResourceStats = { unmanaged?: ResourceCount[]; }; export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, useListJobQuery, + useLazyListJobQuery, useCreateJobMutation, useDeletecollectionJobMutation, useGetJobQuery, + useLazyGetJobQuery, useReplaceJobMutation, useDeleteJobMutation, + useUpdateJobMutation, useListRepositoryQuery, + useLazyListRepositoryQuery, useCreateRepositoryMutation, useDeletecollectionRepositoryMutation, useGetRepositoryQuery, + useLazyGetRepositoryQuery, useReplaceRepositoryMutation, useDeleteRepositoryMutation, + useUpdateRepositoryMutation, useGetRepositoryFilesQuery, + useLazyGetRepositoryFilesQuery, useGetRepositoryFilesWithPathQuery, + useLazyGetRepositoryFilesWithPathQuery, useReplaceRepositoryFilesWithPathMutation, useCreateRepositoryFilesWithPathMutation, useDeleteRepositoryFilesWithPathMutation, useGetRepositoryHistoryQuery, + useLazyGetRepositoryHistoryQuery, useGetRepositoryHistoryWithPathQuery, + useLazyGetRepositoryHistoryWithPathQuery, useGetRepositoryJobsQuery, + useLazyGetRepositoryJobsQuery, useCreateRepositoryJobsMutation, useGetRepositoryJobsWithPathQuery, + useLazyGetRepositoryJobsWithPathQuery, useGetRepositoryRefsQuery, + useLazyGetRepositoryRefsQuery, useGetRepositoryRenderWithPathQuery, + useLazyGetRepositoryRenderWithPathQuery, useGetRepositoryResourcesQuery, + useLazyGetRepositoryResourcesQuery, useGetRepositoryStatusQuery, + useLazyGetRepositoryStatusQuery, useReplaceRepositoryStatusMutation, + useUpdateRepositoryStatusMutation, useCreateRepositoryTestMutation, useGetRepositoryWebhookQuery, + useLazyGetRepositoryWebhookQuery, useCreateRepositoryWebhookMutation, useGetFrontendSettingsQuery, + useLazyGetFrontendSettingsQuery, useGetResourceStatsQuery, + useLazyGetResourceStatsQuery, } = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1beta1/baseAPI.ts similarity index 82% rename from packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/baseAPI.ts rename to packages/grafana-api-clients/src/clients/rtkq/shorturl/v1beta1/baseAPI.ts index 4d1f28bd5d8..c5e91c9f452 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/baseAPI.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1beta1/baseAPI.ts @@ -4,11 +4,11 @@ import { getAPIBaseURL } from '../../../../utils/utils'; import { createBaseQuery } from '../../createBaseQuery'; export const API_GROUP = 'shorturl.grafana.app' as const; -export const API_VERSION = 'v1alpha1' as const; +export const API_VERSION = 'v1beta1' as const; export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION); export const api = createApi({ - reducerPath: 'shortURLAPIv1alpha1', + reducerPath: 'shortURLAPIv1beta1', baseQuery: createBaseQuery({ baseURL: BASE_URL, }), diff --git a/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1beta1/endpoints.gen.ts similarity index 98% rename from packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/endpoints.gen.ts rename to packages/grafana-api-clients/src/clients/rtkq/shorturl/v1beta1/endpoints.gen.ts index 9cabcb8ab7b..1305d03dcc6 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1beta1/endpoints.gen.ts @@ -7,7 +7,7 @@ const injectedRtkApi = api .injectEndpoints({ endpoints: (build) => ({ getApiResources: build.query({ - query: () => ({ url: `/apis/shorturl.grafana.app/v1alpha1/` }), + query: () => ({ url: `/` }), providesTags: ['API Discovery'], }), listShortUrl: build.query({ @@ -597,3 +597,22 @@ export type Patch = object; export type GetGoto = { url: string; }; +export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, + useListShortUrlQuery, + useLazyListShortUrlQuery, + useCreateShortUrlMutation, + useDeletecollectionShortUrlMutation, + useGetShortUrlQuery, + useLazyGetShortUrlQuery, + useReplaceShortUrlMutation, + useDeleteShortUrlMutation, + useUpdateShortUrlMutation, + useGetShortUrlGotoQuery, + useLazyGetShortUrlGotoQuery, + useGetShortUrlStatusQuery, + useLazyGetShortUrlStatusQuery, + useReplaceShortUrlStatusMutation, + useUpdateShortUrlStatusMutation, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/shorturl/v1beta1/index.ts similarity index 100% rename from packages/grafana-api-clients/src/clients/rtkq/shorturl/v1alpha1/index.ts rename to packages/grafana-api-clients/src/clients/rtkq/shorturl/v1beta1/index.ts diff --git a/packages/grafana-api-clients/src/generator/plopfile.ts b/packages/grafana-api-clients/src/generator/plopfile.ts index ed3953bfdc7..3f40c4adc42 100644 --- a/packages/grafana-api-clients/src/generator/plopfile.ts +++ b/packages/grafana-api-clients/src/generator/plopfile.ts @@ -107,15 +107,20 @@ export default function plopGenerator(plop: NodePlopAPI) { return actions; }; - // Read files from data/openapi directory and use as an array of API clients that the user can select + const getOpenAPISpecs = (isEnterprise: boolean): string[] => { + const openapiDir = isEnterprise + ? path.join(basePath, 'pkg/extensions/apiserver/tests/openapi_snapshots') + : path.join(basePath, 'pkg/tests/apis/openapi_snapshots'); - const openapiDir = path.join(basePath, 'data/openapi'); - let possibleOpenAPISpecs: string[] = []; - try { - possibleOpenAPISpecs = fs.readdirSync(openapiDir).filter((file: string) => file.endsWith('.json')); - } catch (e) { - possibleOpenAPISpecs = []; - } + try { + const files = fs.readdirSync(openapiDir).filter((file: string) => file.endsWith('.json')); + return files; + } catch (e) { + throw new Error( + "No OpenAPI specs found! Are you trying to generate an API client for enterprise but haven't linked your local environment?" + ); + } + }; const generator: PlopGeneratorConfig = { description: 'Generate RTK Query API client for a Grafana API group', @@ -129,7 +134,9 @@ export default function plopGenerator(plop: NodePlopAPI) { { type: 'list', loop: false, - choices: possibleOpenAPISpecs, + choices: (answers: { isEnterprise?: boolean }) => { + return getOpenAPISpecs(answers.isEnterprise ?? false); + }, pageSize: 50, name: 'apiInfo', message: 'OpenAPI spec:', diff --git a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts index 82e1339f9bd..8b211fbca8c 100644 --- a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts +++ b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts @@ -17,6 +17,12 @@ type OperationDefinition = { }; type EndpointMatcher = string[] | ((operationName: string, operationDefinition: OperationDefinition) => boolean); +const defaultHooksOptions = { + queries: true, + lazyQueries: true, + mutations: true, +}; + /** * Helper to return consistent base API generation config */ @@ -28,6 +34,7 @@ const createAPIConfig = (app: string, version: string, filterEndpoints?: Endpoin apiFile: `../clients/rtkq/${app}/${version}/baseAPI.ts`, filterEndpoints, tag: true, + hooks: defaultHooksOptions, ...additional, }, }; @@ -39,10 +46,24 @@ const config: ConfigFile = { exportName: 'generatedAPI', outputFiles: { + // OpenAPI3 client with all endpoints + '../clients/rtkq/legacy/endpoints.gen.ts': { + schemaFile: path.join(basePath, 'public/openapi3.json'), + hooks: defaultHooksOptions, + tag: true, + apiFile: '../clients/rtkq/legacy/baseAPI.ts', + filterEndpoints: (_name, operation) => !operation.operation.deprecated, + endpointOverrides: [ + { + pattern: 'listTeamsRoles', + type: 'query', + }, + ], + }, '../clients/rtkq/migrate-to-cloud/endpoints.gen.ts': { schemaFile: path.join(basePath, 'public/openapi3.json'), apiFile: '../clients/rtkq/migrate-to-cloud/baseAPI.ts', - hooks: true, + hooks: defaultHooksOptions, filterEndpoints: [ 'getSessionList', 'getSession', @@ -67,45 +88,29 @@ const config: ConfigFile = { }, '../clients/rtkq/preferences/user/endpoints.gen.ts': { schemaFile: path.join(basePath, 'public/openapi3.json'), - hooks: true, + hooks: defaultHooksOptions, apiFile: '../clients/rtkq/preferences/user/baseAPI.ts', filterEndpoints: ['getUserPreferences', 'updateUserPreferences', 'patchUserPreferences'], }, '../clients/rtkq/user/endpoints.gen.ts': { schemaFile: path.join(basePath, 'public/openapi3.json'), - hooks: true, + hooks: defaultHooksOptions, apiFile: '../clients/rtkq/user/baseAPI.ts', filterEndpoints: ['starDashboardByUid', 'unstarDashboardByUid'], }, - ...createAPIConfig('iam', 'v0alpha1', ['getDisplayMapping']), - ...createAPIConfig('provisioning', 'v0alpha1', filterEndpoints, { hooks: true }), - ...createAPIConfig('folder', 'v1beta1', undefined), - ...createAPIConfig('advisor', 'v0alpha1', [ - 'createCheck', - 'getCheck', - 'listCheck', - 'deleteCheck', - 'updateCheck', - 'listCheckType', - 'updateCheckType', - ]), - ...createAPIConfig('playlist', 'v0alpha1', [ - 'listPlaylist', - 'getPlaylist', - 'createPlaylist', - 'deletePlaylist', - 'replacePlaylist', - ]), - ...createAPIConfig('shorturl', 'v1alpha1'), - ...createAPIConfig('preferences', 'v1alpha1', undefined, { hooks: true }), - ...createAPIConfig('dashboard', 'v0alpha1', ['getSearch']), + ...createAPIConfig('advisor', 'v0alpha1'), ...createAPIConfig('correlations', 'v0alpha1'), + ...createAPIConfig('dashboard', 'v0alpha1'), + ...createAPIConfig('folder', 'v1beta1'), + ...createAPIConfig('iam', 'v0alpha1'), + ...createAPIConfig('playlist', 'v0alpha1'), + ...createAPIConfig('preferences', 'v1alpha1'), + ...createAPIConfig('provisioning', 'v0alpha1'), + ...createAPIConfig('shorturl', 'v1beta1'), + ...createAPIConfig('shorturl', 'v1beta1'), + ...createAPIConfig('shorturl', 'v1beta1'), // PLOP_INJECT_API_CLIENT - Used by the API client generator }, }; -function filterEndpoints(name: string) { - return !name.toLowerCase().includes('getapiresources') && !name.toLowerCase().includes('update'); -} - export default config; diff --git a/packages/grafana-api-clients/src/scripts/process-specs.ts b/packages/grafana-api-clients/src/scripts/process-specs.ts index f3de40bf6ed..5503b24408a 100644 --- a/packages/grafana-api-clients/src/scripts/process-specs.ts +++ b/packages/grafana-api-clients/src/scripts/process-specs.ts @@ -23,7 +23,7 @@ function processOpenAPISpec(spec: OpenAPIV3.Document) { continue; } // Remove the specified part from the path key - const newPathKey = path.replace(/^\/apis\/[^\/]+\/[^\/]+\/namespaces\/\{namespace}/, ''); + const newPathKey = path.replace(/^\/apis\/[^\/]+\/[^\/]+/, '').replace(/^\/namespaces\/\{namespace}/, ''); // Process each method in the path (e.g., get, post) const newPathItem: Record = {}; diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 950d5122932..2fce041444e 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -56,8 +56,8 @@ }, "dependencies": { "@braintree/sanitize-url": "7.0.1", - "@grafana/i18n": "12.3.0-pre", - "@grafana/schema": "12.3.0-pre", + "@grafana/i18n": "12.4.0-pre", + "@grafana/schema": "12.4.0-pre", "@leeoniya/ufuzzy": "1.0.19", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.3", @@ -89,7 +89,7 @@ "@testing-library/react": "16.3.0", "@types/history": "4.7.11", "@types/lodash": "4.17.20", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "@types/papaparse": "5.3.16", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/packages/grafana-data/src/dataframe/ArrayDataFrame.ts b/packages/grafana-data/src/dataframe/ArrayDataFrame.ts index 8817812b5bd..fdcfed8c039 100644 --- a/packages/grafana-data/src/dataframe/ArrayDataFrame.ts +++ b/packages/grafana-data/src/dataframe/ArrayDataFrame.ts @@ -8,14 +8,14 @@ import { guessFieldTypeForField } from './processDataFrame'; * * @deprecated use arrayToDataFrame */ -export class ArrayDataFrame implements DataFrame { +export class ArrayDataFrame implements DataFrame { fields: Field[] = []; length = 0; name?: string; refId?: string; meta?: QueryResultMeta; - constructor(source: T[], names?: string[]) { + constructor(source: unknown[], names?: string[]) { return arrayToDataFrame(source, names); // returns a standard DataFrame } } @@ -27,7 +27,7 @@ export class ArrayDataFrame implements DataFrame { * * @public */ -export function arrayToDataFrame(source: Array> | unknown[], names?: string[]): DataFrame { +export function arrayToDataFrame(source: unknown[], names?: string[]): DataFrame { const df: DataFrame = { fields: [], length: source.length, diff --git a/packages/grafana-data/src/dataframe/DataFrameView.ts b/packages/grafana-data/src/dataframe/DataFrameView.ts index d4d3c157d62..e911748fa39 100644 --- a/packages/grafana-data/src/dataframe/DataFrameView.ts +++ b/packages/grafana-data/src/dataframe/DataFrameView.ts @@ -23,7 +23,7 @@ export class DataFrameView extends FunctionalVector { constructor(private data: DataFrame) { super(); const obj = {} as T; - const fields = {} as any; + const fields: any = {}; for (let i = 0; i < data.fields.length; i++) { const field = data.fields[i]; diff --git a/packages/grafana-data/src/dataframe/MutableDataFrame.ts b/packages/grafana-data/src/dataframe/MutableDataFrame.ts index 4437aa44b5e..6ac9b79e38a 100644 --- a/packages/grafana-data/src/dataframe/MutableDataFrame.ts +++ b/packages/grafana-data/src/dataframe/MutableDataFrame.ts @@ -13,6 +13,8 @@ export type MutableField = Field; /** @deprecated */ type MutableVectorCreator = (buffer?: unknown[]) => unknown[]; +type Parser = (v: string) => unknown; + export const MISSING_VALUE = undefined; // Treated as connected in new graph panel /** @@ -149,14 +151,14 @@ export class MutableDataFrame extends FunctionalVector implements Da } } - private parsers: Map any> | undefined = undefined; + private parsers: Map | undefined = undefined; /** * @deprecated unclear if this is actually used */ - setParser(field: Field, parser: (v: string) => any) { + setParser(field: Field, parser: Parser) { if (!this.parsers) { - this.parsers = new Map any>(); + this.parsers = new Map(); } this.parsers.set(field, parser); return parser; @@ -222,7 +224,7 @@ export class MutableDataFrame extends FunctionalVector implements Da */ add(value: T): void { // Will add one value for every field - const obj = value as any; + const obj: any = value; for (const field of this.fields) { let val = obj[field.name]; @@ -243,7 +245,7 @@ export class MutableDataFrame extends FunctionalVector implements Da throw new Error('Unable to set value beyond current length'); } - const obj = (value as Record) || {}; + const obj: Record = value || {}; for (const field of this.fields) { field.values[index] = obj[field.name]; } diff --git a/packages/grafana-data/src/dataframe/StreamingDataFrame.ts b/packages/grafana-data/src/dataframe/StreamingDataFrame.ts index bb410406ed9..8d37543f73e 100644 --- a/packages/grafana-data/src/dataframe/StreamingDataFrame.ts +++ b/packages/grafana-data/src/dataframe/StreamingDataFrame.ts @@ -420,9 +420,7 @@ export class StreamingDataFrame implements DataFrame { }; getMatchingFieldIndexes = (fieldPredicate: (f: Field) => boolean): number[] => - this.fields - .map((f, index) => (fieldPredicate(f) ? index : undefined)) - .filter((val) => val !== undefined) as number[]; + this.fields.map((f, index) => (fieldPredicate(f) ? index : undefined)).filter((val) => val !== undefined); getValuesFromLastPacket = (): unknown[][] => this.fields.map((f) => { diff --git a/packages/grafana-data/src/dataframe/processDataFrame.test.ts b/packages/grafana-data/src/dataframe/processDataFrame.test.ts index 5c7593e91c0..bb9ffefefcd 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.test.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.test.ts @@ -348,11 +348,11 @@ describe('SeriesData backwards compatibility', () => { expect(isDataFrame(timeseries)).toBeFalsy(); expect(isDataFrame(series)).toBeTruthy(); - const roundtrip = toLegacyResponseData(series) as any; + const roundtrip = toLegacyResponseData(series); expect(isDataFrame(roundtrip)).toBeFalsy(); - expect(roundtrip.type).toBe('docs'); - expect(roundtrip.target).toBe('docs'); - expect(roundtrip.filterable).toBeTruthy(); + expect('type' in roundtrip && roundtrip.type).toBe('docs'); + expect('target' in roundtrip && roundtrip.target).toBe('docs'); + expect('filterable' in roundtrip && roundtrip.filterable).toBeTruthy(); }); }); diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts index 14621547725..c3fd65486c7 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.ts @@ -26,13 +26,13 @@ import { dataFrameFromJSON } from './DataFrameJSON'; function convertTableToDataFrame(table: TableData): DataFrame { const fields = table.columns.map((c) => { // TODO: should be Column but type does not exists there so not sure whats up here. - const { text, type, ...disp } = c as any; + const { text, type, ...disp } = c as Column & { type?: FieldType }; const values: unknown[] = []; return { - name: text?.length ? text : c, // rename 'text' to the 'name' field - config: (disp || {}) as FieldConfig, + name: text ?? c, // rename 'text' to the 'name' field + config: disp || {}, values, - type: type && Object.values(FieldType).includes(type as FieldType) ? (type as FieldType) : FieldType.other, + type: type && Object.values(FieldType).includes(type) ? type : FieldType.other, }; }); @@ -400,8 +400,9 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData = if (config) { // keep unit etc const { ...column } = config; - (column as Column).text = name; - return column as Column; + const result = column as Column; + result.text = name; + return result; } return { text: name }; }), diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index e058612a3c7..a3ef43c91e8 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -435,6 +435,7 @@ export { isStandardFieldProp, type OptionDefaults, } from './panel/getPanelOptionsWithDefaults'; +export { type PanelDataSummary, getPanelDataSummary } from './panel/suggestions/getPanelDataSummary'; export { createFieldConfigRegistry } from './panel/registryFactories'; export { type QueryRunner, type QueryRunnerOptions } from './types/queryRunner'; export { type GroupingToMatrixTransformerOptions } from './transformations/transformers/groupingToMatrix'; @@ -651,7 +652,6 @@ export { type AngularPanelMenuItem, type PanelPluginDataSupport, type VisualizationSuggestion, - type PanelDataSummary, type VisualizationSuggestionsSupplier, VizOrientation, VisualizationSuggestionScore, diff --git a/packages/grafana-data/src/panel/PanelPlugin.ts b/packages/grafana-data/src/panel/PanelPlugin.ts index 61985074be9..4822d4c6acb 100644 --- a/packages/grafana-data/src/panel/PanelPlugin.ts +++ b/packages/grafana-data/src/panel/PanelPlugin.ts @@ -122,11 +122,6 @@ export class PanelPlugin< alertStates: false, }; - /** - * Legacy angular ctrl. If this exists it will be used instead of the panel - */ - angularPanelCtrl?: any; - constructor(panel: ComponentType> | null) { super(); this.panel = panel; diff --git a/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.test.ts b/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.test.ts new file mode 100644 index 00000000000..bef7a302757 --- /dev/null +++ b/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.test.ts @@ -0,0 +1,94 @@ +import { createDataFrame } from '../../dataframe/processDataFrame'; +import { FieldType } from '../../types/dataFrame'; + +import { getPanelDataSummary } from './getPanelDataSummary'; + +describe('getPanelDataSummary', () => { + describe('when called with no dataframes', () => { + it('should return summary with zero counts', () => { + const summary = getPanelDataSummary(); + + expect(summary.rowCountTotal).toBe(0); + expect(summary.rowCountMax).toBe(0); + expect(summary.fieldCount).toBe(0); + expect(summary.frameCount).toBe(0); + expect(summary.hasData).toBe(false); + + expect(summary.fieldCountByType(FieldType.time)).toBe(0); + expect(summary.fieldCountByType(FieldType.number)).toBe(0); + expect(summary.fieldCountByType(FieldType.string)).toBe(0); + expect(summary.fieldCountByType(FieldType.boolean)).toBe(0); + + expect(summary.hasFieldType(FieldType.time)).toBe(false); + expect(summary.hasFieldType(FieldType.number)).toBe(false); + expect(summary.hasFieldType(FieldType.string)).toBe(false); + expect(summary.hasFieldType(FieldType.boolean)).toBe(false); + }); + }); + + describe('when called with a single dataframes', () => { + it('should return correct summary', () => { + const frames = [ + createDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [1, 2, 3] }, + { name: 'value', type: FieldType.number, values: [10, 20, 30] }, + ], + }), + ]; + const summary = getPanelDataSummary(frames); + + expect(summary.rowCountTotal).toBe(3); + expect(summary.rowCountMax).toBe(3); + expect(summary.fieldCount).toBe(2); + expect(summary.frameCount).toBe(1); + expect(summary.hasData).toBe(true); + + expect(summary.fieldCountByType(FieldType.time)).toBe(1); + expect(summary.fieldCountByType(FieldType.number)).toBe(1); + expect(summary.fieldCountByType(FieldType.string)).toBe(0); + expect(summary.fieldCountByType(FieldType.boolean)).toBe(0); + + expect(summary.hasFieldType(FieldType.time)).toBe(true); + expect(summary.hasFieldType(FieldType.number)).toBe(true); + expect(summary.hasFieldType(FieldType.string)).toBe(false); + expect(summary.hasFieldType(FieldType.boolean)).toBe(false); + }); + }); + + describe('when called with multiple dataframes', () => { + it('should return correct summary', () => { + const frames = [ + createDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [1, 2, 3] }, + { name: 'value', type: FieldType.number, values: [10, 20, 30] }, + ], + }), + createDataFrame({ + fields: [ + { name: 'category', type: FieldType.string, values: ['A', 'B'] }, + { name: 'amount', type: FieldType.number, values: [100, 200] }, + ], + }), + ]; + const summary = getPanelDataSummary(frames); + + expect(summary.rowCountTotal).toBe(5); + expect(summary.rowCountMax).toBe(3); + expect(summary.fieldCount).toBe(4); + expect(summary.frameCount).toBe(2); + expect(summary.hasData).toBe(true); + + expect(summary.fieldCountByType(FieldType.time)).toBe(1); + expect(summary.fieldCountByType(FieldType.number)).toBe(2); + expect(summary.fieldCountByType(FieldType.string)).toBe(1); + expect(summary.fieldCountByType(FieldType.boolean)).toBe(0); + + expect(summary.hasFieldType(FieldType.time)).toBe(true); + expect(summary.hasFieldType(FieldType.number)).toBe(true); + expect(summary.hasFieldType(FieldType.string)).toBe(true); + expect(summary.hasFieldType(FieldType.boolean)).toBe(false); + }); + }); +}); diff --git a/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts b/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts new file mode 100644 index 00000000000..ea97e1705d7 --- /dev/null +++ b/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts @@ -0,0 +1,82 @@ +import { PreferredVisualisationType } from '../../types/data'; +import { DataFrame, FieldType } from '../../types/dataFrame'; + +/** + * @alpha + */ +export interface PanelDataSummary { + hasData?: boolean; + rowCountTotal: number; + rowCountMax: number; + frameCount: number; + fieldCount: number; + fieldCountByType: (type: FieldType) => number; + hasFieldType: (type: FieldType) => boolean; + /** The first frame that set's this value */ + preferredVisualisationType?: PreferredVisualisationType; + + /* --- DEPRECATED FIELDS BELOW --- */ + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */ + numberFieldCount: number; + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.time) */ + timeFieldCount: number; + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */ + stringFieldCount: number; + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */ + hasNumberField?: boolean; + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */ + hasTimeField?: boolean; + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */ + hasStringField?: boolean; +} + +/** + * @alpha + * given a list of dataframes, summarize attributes of those frames for features like suggestions. + * @param frames - dataframes to summarize + * @returns summary of the dataframes + */ +export function getPanelDataSummary(frames: DataFrame[] = []): PanelDataSummary { + let rowCountTotal = 0; + let rowCountMax = 0; + let fieldCount = 0; + const countByType: Partial> = {}; + let preferredVisualisationType: PreferredVisualisationType | undefined; + + for (const frame of frames) { + rowCountTotal += frame.length; + + if (frame.meta?.preferredVisualisationType) { + preferredVisualisationType = frame.meta.preferredVisualisationType; + } + + for (const field of frame.fields) { + fieldCount++; + countByType[field.type] = (countByType[field.type] || 0) + 1; + } + + if (frame.length > rowCountMax) { + rowCountMax = frame.length; + } + } + + const fieldCountByType = (f: FieldType) => countByType[f] ?? 0; + + return { + rowCountTotal, + rowCountMax, + fieldCount, + preferredVisualisationType, + frameCount: frames.length, + hasData: rowCountTotal > 0, + hasFieldType: (f: FieldType) => fieldCountByType(f) > 0, + fieldCountByType, + // deprecated + numberFieldCount: fieldCountByType(FieldType.number), + timeFieldCount: fieldCountByType(FieldType.time), + stringFieldCount: fieldCountByType(FieldType.string), + hasTimeField: fieldCountByType(FieldType.time) > 0, + hasNumberField: fieldCountByType(FieldType.number) > 0, + hasStringField: fieldCountByType(FieldType.string) > 0, + }; +} diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 92991080976..376229c83f4 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -291,6 +291,7 @@ export interface GrafanaConfig { rudderstackIntegrationsUrl: string; applicationInsightsConnectionString: string; applicationInsightsEndpointUrl: string; + applicationInsightsAutoRouteTracking: boolean; analyticsConsoleReporting: boolean; rendererAvailable: boolean; rendererVersion: string; @@ -311,6 +312,7 @@ export interface GrafanaConfig { pluginCatalogHiddenPlugins: string[]; pluginCatalogManagedPlugins: string[]; pluginCatalogPreinstalledPlugins: PreinstalledPlugin[]; + pluginCatalogPreinstalledAutoUpdate?: boolean; pluginsCDNBaseURL: string; tokenExpirationDayLimit: number; listDashboardScopesEndpoint: string; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 2855555733a..a0f00b98b19 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -165,15 +165,6 @@ export interface FeatureToggles { */ extraThemes?: boolean; /** - * Enables the plugins frontend sandbox - */ - pluginsFrontendSandbox?: boolean; - /** - * Enables writing multiple items from a single query within Recorded Queries - * @default true - */ - recordedQueriesMulti?: boolean; - /** * A table visualisation for logs in Explore * @default true */ @@ -252,11 +243,6 @@ export interface FeatureToggles { */ externalServiceAccounts?: boolean; /** - * Enables panel monitoring through logs and measurements - * @default true - */ - panelMonitoring?: boolean; - /** * Enables native HTTP Histograms */ enableNativeHTTPHistogram?: boolean; @@ -377,10 +363,6 @@ export interface FeatureToggles { */ annotationPermissionUpdate?: boolean; /** - * Make sure extracted field names are unique in the dataframe - */ - extractFieldsNameDeduplication?: boolean; - /** * Enables dashboard rendering using Scenes for viewer roles * @default true */ @@ -569,10 +551,14 @@ export interface FeatureToggles { */ queryLibrary?: boolean; /** - * Enable suggested dashboards when creating new dashboards + * Enable dashboard library experiments that are production ready */ dashboardLibrary?: boolean; /** + * Enable suggested dashboards when creating new dashboards + */ + suggestedDashboards?: boolean; + /** * Sets the logs table as default visualisation in logs explore */ logsExploreTableDefaultVisualization?: boolean; @@ -729,16 +715,15 @@ export interface FeatureToggles { */ timeRangeProvider?: boolean; /** + * Enables time range panning functionality + */ + timeRangePan?: boolean; + /** * Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. * @default false */ azureMonitorDisableLogLimit?: boolean; /** - * Enables automatic updates for pre-installed plugins - * @default true - */ - preinstallAutoUpdate?: boolean; - /** * Enables experimental reconciler for playlists */ playlistsReconciler?: boolean; @@ -883,7 +868,8 @@ export interface FeatureToggles { */ fetchRulesUsingPost?: boolean; /** - * Enables the new logs panel in Explore + * Enables the new logs panel + * @default true */ newLogsPanel?: boolean; /** @@ -1077,6 +1063,7 @@ export interface FeatureToggles { alertingImportAlertmanagerUI?: boolean; /** * Enables image sharing functionality for dashboards + * @default true */ sharingDashboardImage?: boolean; /** @@ -1123,11 +1110,6 @@ export interface FeatureToggles { */ unifiedStorageSearchDualReaderEnabled?: boolean; /** - * Enables adhoc filtering support for the dashboard datasource - * @default true - */ - dashboardDsAdHocFiltering?: boolean; - /** * Supports __from and __to macros that always use the dashboard level time range */ dashboardLevelTimeMacros?: boolean; @@ -1141,11 +1123,6 @@ export interface FeatureToggles { */ restrictedPluginApis?: boolean; /** - * Enable adhoc filter buttons in visualization tooltips - * @default true - */ - adhocFiltersInTooltips?: boolean; - /** * Enable favorite datasources */ favoriteDatasources?: boolean; @@ -1223,6 +1200,10 @@ export interface FeatureToggles { */ preventPanelChromeOverflow?: boolean; /** + * Enable querying trace data through Jaeger's gRPC endpoint (HTTP) + */ + jaegerEnableGrpcEndpoint?: boolean; + /** * Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded * @default false */ @@ -1232,4 +1213,17 @@ export interface FeatureToggles { * @default true */ onlyStoreActionSets?: boolean; + /** + * Enables a new panel time settings drawer + */ + panelTimeSettings?: boolean; + /** + * Enable template dashboards + */ + dashboardTemplates?: boolean; + /** + * Enables app platform API for annotations + * @default false + */ + kubernetesAnnotations?: boolean; } diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 57e086c1ba2..b49be0d5363 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -162,6 +162,7 @@ export const availableIconsIndex = { globe: true, grafana: true, 'graph-bar': true, + 'hand-pointer': true, heart: true, 'heart-rate': true, 'heart-break': true, @@ -255,6 +256,7 @@ export const availableIconsIndex = { 'tag-alt': true, 'telegram-alt': true, 'text-fields': true, + 'thumbs-down': true, 'thumbs-up': true, times: true, 'times-circle': true, @@ -286,6 +288,7 @@ export const availableIconsIndex = { bitbucket: true, git: true, 'tachometer-fast': true, + 'cmab-logo': true, }; export type IconName = keyof typeof availableIconsIndex; diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index 91083493c83..426c52abadf 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -2,14 +2,15 @@ import { defaultsDeep } from 'lodash'; import { EventBus } from '../events/types'; import { StandardEditorProps } from '../field/standardFieldConfigEditorRegistry'; +import { PanelDataSummary, getPanelDataSummary } from '../panel/suggestions/getPanelDataSummary'; import { Registry } from '../utils/Registry'; import { OptionsEditorItem } from './OptionsUIRegistryBuilder'; import { ScopedVars } from './ScopedVars'; import { AlertStateInfo } from './alerts'; import { PanelModel } from './dashboard'; -import { LoadingState, PreferredVisualisationType } from './data'; -import { DataFrame, FieldType } from './dataFrame'; +import { LoadingState } from './data'; +import { DataFrame } from './dataFrame'; import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource'; import { FieldConfigSource } from './fieldOverrides'; import { IconName } from './icon'; @@ -258,25 +259,6 @@ export enum VisualizationSuggestionScore { OK = 50, } -/** - * @alpha - */ -export interface PanelDataSummary { - hasData?: boolean; - rowCountTotal: number; - rowCountMax: number; - frameCount: number; - fieldCount: number; - numberFieldCount: number; - timeFieldCount: number; - stringFieldCount: number; - hasNumberField?: boolean; - hasTimeField?: boolean; - hasStringField?: boolean; - /** The first frame that set's this value */ - preferredVisualisationType?: PreferredVisualisationType; -} - /** * @alpha */ @@ -293,68 +275,13 @@ export class VisualizationSuggestionsBuilder { constructor(data?: PanelData, panel?: PanelModel) { this.data = data; this.panel = panel; - this.dataSummary = this.computeDataSummary(); + this.dataSummary = getPanelDataSummary(this.data?.series); } getListAppender(defaults: VisualizationSuggestion) { return new VisualizationSuggestionsListAppender(this.list, defaults); } - private computeDataSummary() { - const frames = this.data?.series || []; - - let numberFieldCount = 0; - let timeFieldCount = 0; - let stringFieldCount = 0; - let rowCountTotal = 0; - let rowCountMax = 0; - let fieldCount = 0; - let preferredVisualisationType: PreferredVisualisationType | undefined; - - for (const frame of frames) { - rowCountTotal += frame.length; - - if (frame.meta?.preferredVisualisationType) { - preferredVisualisationType = frame.meta.preferredVisualisationType; - } - - for (const field of frame.fields) { - fieldCount++; - - switch (field.type) { - case FieldType.number: - numberFieldCount += 1; - break; - case FieldType.time: - timeFieldCount += 1; - break; - case FieldType.string: - stringFieldCount += 1; - break; - } - } - - if (frame.length > rowCountMax) { - rowCountMax = frame.length; - } - } - - return { - numberFieldCount, - timeFieldCount, - stringFieldCount, - rowCountTotal, - rowCountMax, - fieldCount, - preferredVisualisationType, - frameCount: frames.length, - hasData: rowCountTotal > 0, - hasTimeField: timeFieldCount > 0, - hasNumberField: numberFieldCount > 0, - hasStringField: stringFieldCount > 0, - }; - } - getList() { return this.list; } diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 47e130afcce..4b988c49a0a 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -230,6 +230,7 @@ export enum PluginExtensionPointPatterns { // Extension Points available in plugins export enum PluginExtensionExposedComponents { CentralAlertHistorySceneV1 = 'grafana/central-alert-history-scene/v1', + AddToDashboardFormV1 = 'grafana/add-to-dashboard-form/v1', } export type PluginExtensionPanelContext = { diff --git a/packages/grafana-data/src/types/scopes.ts b/packages/grafana-data/src/types/scopes.ts index 98b32dfa0c7..e5897c137da 100644 --- a/packages/grafana-data/src/types/scopes.ts +++ b/packages/grafana-data/src/types/scopes.ts @@ -70,7 +70,7 @@ export type ScopeNodeLinkType = 'scope'; export interface ScopeNodeSpec { nodeType: ScopeNodeNodeType; title: string; - + subTitle?: string; description?: string; // If true for a scope category/type, it means only single child can be selected at a time. diff --git a/packages/grafana-data/src/utils/csv.ts b/packages/grafana-data/src/utils/csv.ts index 7c225e47ac0..9823aa4c550 100644 --- a/packages/grafana-data/src/utils/csv.ts +++ b/packages/grafana-data/src/utils/csv.ts @@ -115,7 +115,7 @@ export class CSVReader { if (!fields[j].config) { fields[j].config = {}; } - const disp = fields[j].config as any; // any lets name lookup + const disp: any = fields[j].config; // any lets name lookup disp[k] = j === 0 ? v : line[j]; } } diff --git a/packages/grafana-data/src/utils/dataLinks.test.ts b/packages/grafana-data/src/utils/dataLinks.test.ts index 86e01f4139c..379e678fd90 100644 --- a/packages/grafana-data/src/utils/dataLinks.test.ts +++ b/packages/grafana-data/src/utils/dataLinks.test.ts @@ -46,7 +46,7 @@ describe('mapInternalLinkToExplore', () => { expect(link).toEqual( expect.objectContaining({ title: 'dsName', - href: `/explore?left=${encodeURIComponent('{"datasource":"uid","queries":[{"query":"12344"}]}')}`, + href: `/explore?left=${encodeURIComponent('{"datasource":"uid","queries":[{"query":"12344","datasource":{"uid":"uid"}}]}')}`, onClick: undefined, interpolatedParams: { query: { @@ -95,7 +95,7 @@ describe('mapInternalLinkToExplore', () => { expect.objectContaining({ title: 'dsName', href: `/explore?left=${encodeURIComponent( - '{"datasource":"uid","queries":[{"query":"12344"}],"panelsState":{"trace":{"spanId":"abcdef"}}}' + '{"datasource":"uid","queries":[{"query":"12344","datasource":{"uid":"uid"}}],"panelsState":{"trace":{"spanId":"abcdef"}}}' )}`, onClick: undefined, }) @@ -144,6 +144,7 @@ describe('mapInternalLinkToExplore', () => { nested: { something: 'val1' }, num: 1, arr: ['val1', 'non var'], + datasource: { uid: 'uid' }, }; expect(decodeURIComponent(link.href)).toEqual( @@ -158,9 +159,6 @@ describe('mapInternalLinkToExplore', () => { ); expect(link.interpolatedParams?.query).toEqual({ - datasource: { - uid: 'uid', - }, ...query, }); diff --git a/packages/grafana-data/src/utils/dataLinks.ts b/packages/grafana-data/src/utils/dataLinks.ts index ece36cf525a..a9b38e1df15 100644 --- a/packages/grafana-data/src/utils/dataLinks.ts +++ b/packages/grafana-data/src/utils/dataLinks.ts @@ -42,6 +42,15 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod typeof link.internal?.query === 'function' ? link.internal.query({ replaceVariables, scopedVars }) : internalLink.query; + + // datasource ref is optional in a query object, but Explore relies on it being defined for some + // functionalities, e.g., changing query filters directly from visualizations, so we need to put + // it here if it's missing. See also #112945 + if (query && typeof query === 'object' && !query.datasource?.uid && internalLink.datasourceUid) { + query.datasource = query.datasource || {}; + query.datasource.uid = internalLink.datasourceUid; + } + const interpolatedQuery = interpolateObject(query, scopedVars, replaceVariables); const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables); const interpolatedCorrelationData = interpolateObject(link.meta?.correlationData, scopedVars, replaceVariables); @@ -49,14 +58,7 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod const interpolatedParams = interpolatedQuery ? { - query: { - ...interpolatedQuery, - // data source is defined in a separate property in DataLink, we ensure it's put back together after interpolation - datasource: { - ...interpolatedQuery.datasource, - uid: internalLink.datasourceUid, - }, - }, + query: interpolatedQuery, ...(range && { timeRange: range }), } : undefined; diff --git a/packages/grafana-data/src/utils/throwIfAngular.test.ts b/packages/grafana-data/src/utils/throwIfAngular.test.ts index 8cdbae71cd9..8b17e2f10e6 100644 --- a/packages/grafana-data/src/utils/throwIfAngular.test.ts +++ b/packages/grafana-data/src/utils/throwIfAngular.test.ts @@ -33,6 +33,7 @@ describe('throwIfAngular', () => { it('should throw if angular panel', () => { const underTest = new PanelPlugin(null); + // @ts-expect-error underTest.angularPanelCtrl = {}; expect(() => throwIfAngular(underTest)).toThrow('Angular plugins are not supported'); }); diff --git a/packages/grafana-data/src/vector/CircularVector.ts b/packages/grafana-data/src/vector/CircularVector.ts index 221f7c488e5..70b570630bc 100644 --- a/packages/grafana-data/src/vector/CircularVector.ts +++ b/packages/grafana-data/src/vector/CircularVector.ts @@ -16,7 +16,7 @@ interface CircularOptions { * @public * @deprecated use a simple Arrays */ -export class CircularVector extends FunctionalVector { +export class CircularVector extends FunctionalVector { private buffer: T[]; private index: number; private capacity: number; diff --git a/packages/grafana-data/src/vector/FunctionalVector.ts b/packages/grafana-data/src/vector/FunctionalVector.ts index f5ccfc7be8a..a84b6122232 100644 --- a/packages/grafana-data/src/vector/FunctionalVector.ts +++ b/packages/grafana-data/src/vector/FunctionalVector.ts @@ -153,13 +153,13 @@ export abstract class FunctionalVector { findIndex(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): number { return this.toArray().findIndex(predicate, thisArg); } - entries(): IterableIterator<[number, T]> { + entries(): ArrayIterator<[number, T]> { return this.toArray().entries(); } - keys(): IterableIterator { + keys(): ArrayIterator { return this.toArray().keys(); } - values(): IterableIterator { + values(): ArrayIterator { return this.toArray().values(); } includes(searchElement: T, fromIndex?: number | undefined): boolean { diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 008f555cc3b..7a8109b8a57 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", @@ -40,7 +40,7 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "16.0.1", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "@types/semver": "7.7.1", "esbuild": "0.25.8", "rimraf": "6.0.1", diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md index be49122b758..aa6d4e880ff 100644 --- a/packages/grafana-eslint-rules/README.md +++ b/packages/grafana-eslint-rules/README.md @@ -140,3 +140,44 @@ export default storyConfig; const storyConfig = { title: 'Components/Forms/Button' }; export default storyConfig; ``` + +### `no-plugin-external-import-paths` + +Prevent plugins from importing anything outside their own directory. + +This rule enforces strict plugin isolation by preventing plugins from importing anything that reaches outside their own plugin directory. This helps maintain clean plugin boundaries and prevents tight coupling between plugins and other parts of the codebase. + +The rule automatically detects the current plugin directory from the file path and blocks any relative imports that would reach outside that directory. + +The rule is applied to specific plugins by configuring the `files` pattern in the ESLint configuration, similar to `grafana/decoupled-plugins-overrides`. + +#### Examples + +```tsx +// Bad ❌ - Importing from sibling plugin +import { getDataLinks } from '../status-history/utils'; +import { isTooltipScrollable } from '../timeseries/utils'; + +// Bad ❌ - Importing from Grafana core +import { something } from '../../../features/dashboard/state'; + +// Bad ❌ - Importing from outside plugin directory +import { other } from '../some-other-folder/utils'; + +// Good ✅ - Importing from same plugin +import { someUtil } from './utils'; +import { Component } from './Component'; +import { helper } from './subfolder/helper'; + +// Good ✅ - Importing from external packages +import React from 'react'; +import { Button } from '@grafana/ui'; +``` + +#### Error Message + +When a violation is detected, the rule reports: + +``` +Import '../status-history/utils' reaches outside the 'histogram' plugin directory. Plugins should only import from external dependencies or relative paths within their own directory. +``` diff --git a/packages/grafana-eslint-rules/index.cjs b/packages/grafana-eslint-rules/index.cjs index 438d88332bc..89a76ee60a1 100644 --- a/packages/grafana-eslint-rules/index.cjs +++ b/packages/grafana-eslint-rules/index.cjs @@ -4,6 +4,7 @@ const noUnreducedMotion = require('./rules/no-unreduced-motion.cjs'); const themeTokenUsage = require('./rules/theme-token-usage.cjs'); const noRestrictedImgSrcs = require('./rules/no-restricted-img-srcs.cjs'); const consistentStoryTitles = require('./rules/consistent-story-titles.cjs'); +const noPluginExternalImportPaths = require('./rules/no-plugin-external-import-paths.cjs'); module.exports = { rules: { @@ -13,5 +14,6 @@ module.exports = { 'theme-token-usage': themeTokenUsage, 'no-restricted-img-srcs': noRestrictedImgSrcs, 'consistent-story-titles': consistentStoryTitles, + 'no-plugin-external-import-paths': noPluginExternalImportPaths, }, }; diff --git a/packages/grafana-eslint-rules/jest.config.js b/packages/grafana-eslint-rules/jest.config.js new file mode 100644 index 00000000000..757e62524d6 --- /dev/null +++ b/packages/grafana-eslint-rules/jest.config.js @@ -0,0 +1,4 @@ +export default { + testEnvironment: 'node', + testMatch: ['/**/*.test.js'], +}; diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index f182cd0281b..0fe0ca61193 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -1,12 +1,14 @@ { "name": "@grafana/eslint-plugin", "description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.", - "version": "12.3.0-pre", + "version": "12.4.0-pre", + "type": "module", "main": "./index.cjs", "author": "Grafana Labs", "license": "Apache-2.0", "scripts": { - "typecheck": "tsc --emitDeclarationOnly false --noEmit" + "typecheck": "tsc --emitDeclarationOnly false --noEmit", + "test": "NODE_OPTIONS='--experimental-vm-modules' jest" }, "repository": { "type": "git", @@ -19,6 +21,7 @@ "devDependencies": { "@typescript-eslint/types": "^8.9.0", "eslint": "9.32.0", + "jest": "29.7.0", "tslib": "2.8.1" }, "private": true diff --git a/packages/grafana-eslint-rules/rules/no-plugin-external-import-paths.cjs b/packages/grafana-eslint-rules/rules/no-plugin-external-import-paths.cjs new file mode 100644 index 00000000000..63f44fe3077 --- /dev/null +++ b/packages/grafana-eslint-rules/rules/no-plugin-external-import-paths.cjs @@ -0,0 +1,98 @@ +// @ts-check +/** @typedef {import('@typescript-eslint/utils').TSESTree.ImportDeclaration} ImportDeclaration */ +const { ESLintUtils } = require('@typescript-eslint/utils'); +const path = require('path'); + +const createRule = ESLintUtils.RuleCreator( + (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` +); + +/** + * Extract the plugin root directory from the file path + * @param {string} filePath - The file path being linted + * @returns {string|null} - The plugin root directory or null if not in a plugin directory + */ +function getPluginRootDirectory(filePath) { + const pluginMatch = filePath.match(/\/plugins\/(?:panel|datasource)\/([^/]+)\//); + if (pluginMatch) { + const pluginName = pluginMatch[1]; + const pluginType = pluginMatch[0].includes('/panel/') ? 'panel' : 'datasource'; + + const pluginDirPath = `/plugins/${pluginType}/${pluginName}`; + const pluginDirStart = filePath.indexOf(pluginDirPath); + if (pluginDirStart !== -1) { + const pluginRoot = filePath.substring(0, pluginDirStart + pluginDirPath.length); + return path.isAbsolute(pluginRoot) ? pluginRoot : path.resolve(pluginRoot); + } + } + return null; +} + +/** + * Check if an import path reaches outside the plugin's root directory boundaries + * @param {string} importPath - The import path to check + * @param {string} currentFilePath - The current file path being linted + * @param {string} pluginRoot - The plugin root directory + * @returns {boolean} - True if the import goes outside plugin boundaries + */ +function isImportOutsidePluginBoundaries(importPath, currentFilePath, pluginRoot) { + const isRelativeImport = importPath.startsWith('./') || importPath.startsWith('../'); + if (!isRelativeImport) { + return false; + } + + const currentDir = path.dirname(currentFilePath); + const resolvedPath = path.resolve(currentDir, importPath); + + const normalizedResolvedPath = path.normalize(resolvedPath); + const normalizedPluginRoot = path.normalize(pluginRoot); + + return !normalizedResolvedPath.startsWith(normalizedPluginRoot); +} + +const noRestrictedPeerPluginPathsRule = createRule({ + create(context) { + const currentFilePath = context.getFilename(); + const pluginRoot = getPluginRootDirectory(currentFilePath); + + if (!pluginRoot) { + return {}; + } + + return { + /** @param {ImportDeclaration} node */ + ImportDeclaration(node) { + const importPath = node.source.value; + + if ( + typeof importPath === 'string' && + isImportOutsidePluginBoundaries(importPath, currentFilePath, pluginRoot) + ) { + return context.report({ + node: node.source, + messageId: 'importOutsidePluginBoundaries', + data: { + importPath, + pluginRoot: path.basename(pluginRoot), + }, + }); + } + }, + }; + }, + name: 'no-plugin-external-import-paths', + meta: { + type: 'problem', + docs: { + description: 'Disallow imports that reach outside plugin root directory boundaries', + }, + messages: { + importOutsidePluginBoundaries: + "Import '{{importPath}}' reaches outside the '{{pluginRoot}}' plugin directory. Plugins should only import from external dependencies or relative paths within their own directory.", + }, + schema: [], + }, + defaultOptions: [], +}); + +module.exports = noRestrictedPeerPluginPathsRule; diff --git a/packages/grafana-eslint-rules/tests/no-plugin-external-import-paths.test.js b/packages/grafana-eslint-rules/tests/no-plugin-external-import-paths.test.js new file mode 100644 index 00000000000..3c25742d3eb --- /dev/null +++ b/packages/grafana-eslint-rules/tests/no-plugin-external-import-paths.test.js @@ -0,0 +1,81 @@ +import { RuleTester } from 'eslint'; + +import rule from '../rules/no-plugin-external-import-paths.cjs'; + +RuleTester.setDefaultConfig({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, +}); + +const ruleTester = new RuleTester(); + +ruleTester.run('eslint no-plugin-external-import-paths', rule, { + valid: [ + { + name: 'external npm package import', + filename: 'public/app/plugins/panel/histogram/HistogramTooltip.tsx', + code: "import React from 'react';", + }, + { + name: 'grafana package import', + filename: 'public/app/plugins/panel/histogram/HistogramTooltip.tsx', + code: "import { Button } from '@grafana/ui';", + }, + { + name: 'same plugin file import', + filename: 'public/app/plugins/panel/histogram/HistogramTooltip.tsx', + code: "import { someUtil } from './utils';", + }, + { + name: 'same plugin subdirectory import', + filename: 'public/app/plugins/panel/histogram/components/HistogramTooltip.tsx', + code: "import { Component } from '../Component';", + }, + ], + invalid: [ + { + name: 'sibling plugin import', + filename: 'public/app/plugins/panel/histogram/HistogramTooltip.tsx', + code: "import { getDataLinks } from '../status-history/utils';", + errors: [ + { + messageId: 'importOutsidePluginBoundaries', + data: { + importPath: '../status-history/utils', + pluginRoot: 'histogram', + }, + }, + ], + }, + { + name: 'grafana core import', + filename: 'public/app/plugins/panel/histogram/HistogramTooltip.tsx', + code: "import { something } from '../../../features/dashboard/state';", + errors: [ + { + messageId: 'importOutsidePluginBoundaries', + data: { + importPath: '../../../features/dashboard/state', + pluginRoot: 'histogram', + }, + }, + ], + }, + { + name: 'datasource plugin sibling import', + filename: 'public/app/plugins/datasource/loki/datasource.ts', + code: "import { something } from '../prometheus/utils';", + errors: [ + { + messageId: 'importOutsidePluginBoundaries', + data: { + importPath: '../prometheus/utils', + pluginRoot: 'loki', + }, + }, + ], + }, + ], +}); diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 3185d65d5c1..d450f911e70 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/flamegraph", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Grafana flamegraph visualization component", "keywords": [ "grafana", @@ -44,8 +44,8 @@ ], "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.3.0-pre", - "@grafana/ui": "12.3.0-pre", + "@grafana/data": "12.4.0-pre", + "@grafana/ui": "12.4.0-pre", "@leeoniya/ufuzzy": "1.0.19", "d3": "^7.8.5", "lodash": "4.17.21", @@ -67,7 +67,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.20", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.8", "@types/tinycolor2": "1.4.6", @@ -83,7 +83,7 @@ "typescript": "5.9.2" }, "peerDependencies": { - "@grafana/assistant": "^0.1.0", + "@grafana/assistant": "^0.1.1", "react": "^18.0.0", "react-dom": "^18.0.0" } diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx index 10e2eeb8d03..4fdb00d6748 100644 --- a/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx +++ b/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx @@ -55,15 +55,15 @@ describe('FlameGraphContainer', () => { it('should update search when row selected in top table', async () => { render(); await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]); - expect(screen.getByDisplayValue('net/http.HandlerFunc.ServeHTTP')).toBeInTheDocument(); + expect(screen.getByDisplayValue('^net/http\\.HandlerFunc\\.ServeHTTP$')).toBeInTheDocument(); // Unclick the selection so that we can click something else and continue test checks await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]); await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[1]); - expect(screen.getByDisplayValue('total')).toBeInTheDocument(); + expect(screen.getByDisplayValue('^total$')).toBeInTheDocument(); // after it is highlighted it will be the only (first) item in the table so [1] -> [0] await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]); - expect(screen.queryByDisplayValue('total')).not.toBeInTheDocument(); + expect(screen.queryByDisplayValue('^total$')).not.toBeInTheDocument(); }); it('should render options', async () => { diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.tsx index da09ea26739..5d95051e353 100644 --- a/packages/grafana-flamegraph/src/FlameGraphContainer.tsx +++ b/packages/grafana-flamegraph/src/FlameGraphContainer.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import * as React from 'react'; import { useMeasure } from 'react-use'; -import { DataFrame, GrafanaTheme2 } from '@grafana/data'; +import { DataFrame, GrafanaTheme2, escapeStringForRegex } from '@grafana/data'; import { ThemeContext } from '@grafana/ui'; import FlameGraph from './FlameGraph/FlameGraph'; @@ -189,11 +189,13 @@ const FlameGraphContainer = ({ const onSymbolClick = useCallback( (symbol: string) => { - if (search === symbol) { + const anchored = `^${escapeStringForRegex(symbol)}$`; + + if (search === anchored) { setSearch(''); } else { onTableSymbolClick?.(symbol); - setSearch(symbol); + setSearch(anchored); resetFocus(); } }, @@ -241,7 +243,13 @@ const FlameGraphContainer = ({ matchedLabels={matchedLabels} sandwichItem={sandwichItem} onSandwich={setSandwichItem} - onSearch={setSearch} + onSearch={(str) => { + if (!str) { + setSearch(''); + return; + } + setSearch(`^${escapeStringForRegex(str)}$`); + }} onTableSort={onTableSort} colorScheme={colorScheme} /> diff --git a/packages/grafana-flamegraph/src/FlameGraphHeader.tsx b/packages/grafana-flamegraph/src/FlameGraphHeader.tsx index 46f6cde182f..fbeb0ad3c6a 100644 --- a/packages/grafana-flamegraph/src/FlameGraphHeader.tsx +++ b/packages/grafana-flamegraph/src/FlameGraphHeader.tsx @@ -90,7 +90,11 @@ const FlameGraphHeader = ({
{!!assistantContext?.length && (
- +
)} {showResetButton && ( diff --git a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx index 8c5461bb053..b9072f42c29 100644 --- a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx +++ b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx @@ -10,6 +10,7 @@ import { FieldType, GrafanaTheme2, MappingType, + escapeStringForRegex, } from '@grafana/data'; import { IconButton, @@ -332,7 +333,7 @@ type ActionCellProps = { function ActionCell(props: ActionCellProps) { const styles = getStylesActionCell(); const symbol = props.frame.fields.find((f: Field) => f.name === 'Symbol')?.values[props.rowIndex]; - const isSearched = props.search === symbol; + const isSearched = props.search === `^${escapeStringForRegex(String(symbol))}$`; const isSandwiched = props.sandwichItem === symbol; return ( diff --git a/packages/grafana-i18n/package.json b/packages/grafana-i18n/package.json index ba8d2311acf..824f594f8e6 100644 --- a/packages/grafana-i18n/package.json +++ b/packages/grafana-i18n/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/i18n", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Grafana Internationalization Library", "keywords": [ "grafana", diff --git a/packages/grafana-i18n/src/types/dates.d.ts b/packages/grafana-i18n/src/types/dates.d.ts index 7a4ac64930c..3d728f70d85 100644 --- a/packages/grafana-i18n/src/types/dates.d.ts +++ b/packages/grafana-i18n/src/types/dates.d.ts @@ -1,4 +1,4 @@ -import { +import type { DurationFormatConstructor, DurationFormatOptions as _DurationFormatOptions, DurationInput as _DurationInput, diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 5694659ab70..569b7e60c88 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "name": "@grafana/o11y-ds-frontend", "private": true, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Library to manage traces in Grafana.", "sideEffects": false, "repository": { @@ -18,12 +18,12 @@ }, "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.3.0-pre", - "@grafana/e2e-selectors": "12.3.0-pre", + "@grafana/data": "12.4.0-pre", + "@grafana/e2e-selectors": "12.4.0-pre", "@grafana/plugin-ui": "^0.10.10", - "@grafana/runtime": "12.3.0-pre", - "@grafana/schema": "12.3.0-pre", - "@grafana/ui": "12.3.0-pre", + "@grafana/runtime": "12.4.0-pre", + "@grafana/schema": "12.4.0-pre", + "@grafana/ui": "12.4.0-pre", "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", @@ -35,7 +35,7 @@ "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@types/jest": "^29.5.4", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "@types/react": "18.3.18", "@types/systemjs": "6.15.3", "jest": "^29.6.4", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 84a74940dbf..68400c19b92 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -2,7 +2,7 @@ "name": "@grafana/plugin-configs", "description": "Shared dependencies and files for core plugins", "private": true, - "version": "12.3.0-pre", + "version": "12.4.0-pre", "dependencies": { "react": "18.3.1", "terser-webpack-plugin": "5.3.14", diff --git a/packages/grafana-prometheus/i18next.config.ts b/packages/grafana-prometheus/i18next.config.ts new file mode 100644 index 00000000000..8c5a67d2521 --- /dev/null +++ b/packages/grafana-prometheus/i18next.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'i18next-cli'; + +export default defineConfig({ + locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages + extract: { + input: ['src/**/*.{tsx,ts}'], + output: 'src/locales/{{language}}/{{namespace}}.json', + defaultNS: 'grafana-prometheus', + functions: ['t', '*.t'], + transComponents: ['Trans'], + }, +}); diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 772df81b58f..a4d36839bf6 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "AGPL-3.0-only", "name": "@grafana/prometheus", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Grafana Prometheus Library", "keywords": [ "typescript", @@ -33,7 +33,7 @@ "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", - "i18n-extract": "i18next --config src/locales/i18next-parser.config.cjs", + "i18n-extract": "i18next-cli extract --sync-primary", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json" @@ -41,13 +41,13 @@ "dependencies": { "@emotion/css": "11.13.5", "@floating-ui/react": "0.27.16", - "@grafana/data": "12.3.0-pre", - "@grafana/e2e-selectors": "12.3.0-pre", - "@grafana/i18n": "12.3.0-pre", + "@grafana/data": "12.4.0-pre", + "@grafana/e2e-selectors": "12.4.0-pre", + "@grafana/i18n": "12.4.0-pre", "@grafana/plugin-ui": "^0.10.10", - "@grafana/runtime": "12.3.0-pre", - "@grafana/schema": "12.3.0-pre", - "@grafana/ui": "12.3.0-pre", + "@grafana/runtime": "12.4.0-pre", + "@grafana/schema": "12.4.0-pre", + "@grafana/ui": "12.4.0-pre", "@hello-pangea/dnd": "18.0.1", "@leeoniya/ufuzzy": "1.0.19", "@lezer/common": "1.2.3", @@ -87,11 +87,11 @@ "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "esbuild": "0.25.8", - "i18next-parser": "9.3.0", + "i18next-cli": "1.11.12", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "react": "18.3.1", diff --git a/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json index 7afae00fd13..2379436aa18 100644 --- a/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Původní implementace editoru proměnných dotazů Prometheus. Zadejte řetězec se správným typem dotazu a parametry, jak je popsáno v těchto dokumentech. Například {{exampleQuery}}.", "tooltip-label": "Vrátí seznam hodnot štítků pro název štítku ve všech metrikách, pokud není metrika zadána.", "tooltip-metric-regex": "Vrátí seznam názvů štítků, volitelně filtrování podle zadaného metrického regulárního výrazu.", - "tooltip-query": "Vrátí seznam výsledků dotazu Prometheus pro daný dotaz. To může zahrnovat funkce Prometheus, tj. {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Zásuvný modul zdroje dat Prometheus poskytuje následující typy dotazů pro proměnné šablony.", - "tooltip-series-query": "Zadejte metriku se štítky, pouze metriku nebo pouze štítky, tj. {{example1}}, {{example2}} nebo {{example3}}. Vrátí seznam časových řad spojených se zadanými daty." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "volič", @@ -175,16 +175,16 @@ "label-scrape-interval": "Interval scrapingu", "label-series-limit": "Limit řady", "label-use-series-endpoint": "Použít koncový bod řady", - "more-info": "Další informace o konfiguraci typu a verze Prometheus ve zdrojích dat najdete v <2>dokumentaci k zajišťování.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Příklad: {{example}}", "title-interval-behaviour": "Chování intervalu", "title-other": "Ostatní", "title-performance": "Výkon", "title-query-editor": "Editor dotazů", "tooltip-cache-level": "Nastaví úroveň ukládání do vyrovnávací paměti prohlížeče pro dotazy editoru. Pro zdroje dat s vysokou kardinalitou se doporučuje nastavit vyšší úroveň vyrovnávací paměti.", - "tooltip-custom-query-parameters": "Přidejte vlastní parametry do adresy URL dotazu Prometheus. Například {{example1}}, {{example2}}, {{example3}} nebo {{example4}}. Více parametrů by mělo být pospojováno pomocí {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Nastavit výchozí možnost editoru pro všechny uživatele tohoto zdroje dat.", - "tooltip-disable-metrics-lookup": "Zaškrtnutím této možnosti zakážete výběr metrik a podporu metrik/štítků v automatickém doplňování pole dotazu. Užitečné, pokud máte problémy s výkonem u větších instancí Prometheus. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Tato funkce zakáže pravidla nahrávání. Zapněte tuto možnost pro zlepšení výkonu nástěnky", "tooltip-http-method": "K dotazu na zdroj dat Prometheus můžete použít metodu POST nebo GET HTTP. Doporučenou metodou je POST, protože umožňuje větší dotazy. Změňte na GET, pokud máte verzi Prometheus starší než 2.1 nebo pokud jsou požadavky POST ve vaší síti omezeny.", "tooltip-incremental-querying-beta": "Tato funkce změní výchozí chování relativních dotazů tak, aby vždy požadovaly aktuální data z instance Prometheus. Místo toho budou výsledky dotazů uloženy do vyrovnávací paměti a budou požadovány pouze nové záznamy. Povolte tuto možnost pro snížení zatížení databáze a sítě.", @@ -479,7 +479,7 @@ "aria-label-selector": "volič" }, "results-table": { - "content-descriptive-type": "Při vytváření {{descriptiveType}} Prometheus zobrazí více řad s počítadlem typů. ", + "content-descriptive-type": "", "description": "Popis", "message-expand-label-filters": "Nebyly nalezeny žádné metriky. Zkuste rozšířit filtry štítků.", "message-expand-search": "Nebyly nalezeny žádné metriky. Zkuste rozšířit vyhledávání a filtry.", diff --git a/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json index 5b95ca6bdf4..3b63c9cfc00 100644 --- a/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Die ursprüngliche Implementierung des Prometheus-Variablen-Abfrageeditors. Geben Sie eine Zeichenfolge mit dem richtigen Abfragetyp und den richtigen Parametern ein, wie in diesen Dokumenten beschrieben. Zum Beispiel: {{exampleQuery}}.", "tooltip-label": "Gibt eine Liste von Label-Werten für den Label-Namen in allen Metriken zurück, es sei denn, die Metrik ist angegeben.", "tooltip-metric-regex": "Gibt eine Liste von Label-Namen zurück, die optional gemäß dem angegebenen Metrik-Regex gefiltert werden.", - "tooltip-query": "Gibt eine Liste der Prometheus-Abfrageergebnisse für die Abfrage zurück. Dies kann Prometheus-Funktionen umfassen, z. B. {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Das Prometheus-Datenquellen-Plugin bietet die folgenden Abfragetypen für Vorlagenvariablen.", - "tooltip-series-query": "Geben Sie eine Metrik mit Labels ein, nur eine Metrik oder nur Labels, z. B. {{example1}}, {{example2}} oder {{example3}}. Gibt eine Liste der Zeitreihen zurück, die den eingegebenen Daten zugeordnet sind." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "Selektor", @@ -175,16 +175,16 @@ "label-scrape-interval": "Scrape-Intervall", "label-series-limit": "Reihengrenze", "label-use-series-endpoint": "Reihenendpunkt verwenden", - "more-info": "Weitere Informationen zur Konfiguration des Typs und der Version von Prometheus in Datenquellen finden Sie in der <2>Bereitstellungsdokumentation.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Beispiel: {{example}}", "title-interval-behaviour": "Intervallverhalten", "title-other": "Sonstiges", "title-performance": "Leistung", "title-query-editor": "Abfrage-Editor", "tooltip-cache-level": "Legt die Browser-Cache-Level für Editor-Abfragen fest. Für Datenquellen mit hoher Kardinalität werden höhere Cache-Einstellungen empfohlen.", - "tooltip-custom-query-parameters": "Fügen Sie der URL der Prometheus-Abfrage benutzerdefinierte Parameter hinzu. Zum Beispiel {{example1}}, {{example2}}, {{example3}} oder {{example4}}. Mehrere Parameter sollten mit {{concatenationChar}} verknüpft werden.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Legen Sie die Standard-Editoroption für alle Nutzer dieser Datenquelle fest.", - "tooltip-disable-metrics-lookup": "Wenn diese Option aktiviert ist, werden die Metrikauswahl und die Unterstützung für Metriken/Labels im Rahmen der Autovervollständigung des Abfragefelds deaktiviert. Dies hilft, wenn Sie Leistungsprobleme mit größeren Prometheus-Instanzen haben. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Diese Funktion deaktiviert die Aufnahmeregeln. Wenn Sie dies aktivieren, wird die Dashboard-Leistung verbessert", "tooltip-http-method": "Sie können entweder die HTTP-Methode POST oder GET für die Abfrage Ihrer Prometheus-Datenquelle verwenden. POST ist die empfohlene Methode, da sie größere Abfragen ermöglicht. Ändern Sie dies zu GET, wenn Ihre Prometheus-Version älter als 2.1 ist oder POST-Abfragen in Ihrem Netzwerk beschränkt sind.", "tooltip-incremental-querying-beta": "Diese Funktion ändert das Standardverhalten von relativen Abfragen, sodass immer neue Daten von der Prometheus-Instanz abgefragt werden. Stattdessen werden Abfrageergebnisse zwischengespeichert und nur neue Datensätze abgefragt. Wenn Sie dies aktivieren, wird die Datenbank- und Netzwerklast reduziert.", @@ -479,7 +479,7 @@ "aria-label-selector": "Selektor" }, "results-table": { - "content-descriptive-type": "Bei der Erstellung von {{descriptiveType}} zeigt Prometheus mehrere Reihen mit dem Typ Counter. ", + "content-descriptive-type": "", "description": "Beschreibung", "message-expand-label-filters": "Es wurden keine Metriken gefunden. Versuchen Sie, Ihre Label-Filter zu erweitern.", "message-expand-search": "Es wurden keine Metriken gefunden. Versuchen Sie, Ihre Suche und Ihre Filter zu erweitern.", diff --git a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json index 58c601d9514..d21b1c2d693 100644 --- a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "The original implementation of the Prometheus variable query editor. Enter a string with the correct query type and parameters as described in these docs. For example, {{exampleQuery}}.", "tooltip-label": "Returns a list of label values for the label name in all metrics unless the metric is specified.", "tooltip-metric-regex": "Returns a list of label names, optionally filtering by specified metric regex.", - "tooltip-query": "Returns a list of Prometheus query results for the query. This can include Prometheus functions, i.e.{{exampleQuery}}.", + "tooltip-query": "Returns a list of Prometheus query results for the query. This can include Prometheus functions, i.e. {{exampleQuery}}.", "tooltip-query-type": "The Prometheus data source plugin provides the following query types for template variables.", - "tooltip-series-query": "Enter a metric with labels, only a metric or only labels, i.e.{{example1}}, {{example2}}, or {{example3}}. Returns a list of time series associated with the entered data." + "tooltip-series-query": "Enter a metric with labels, only a metric or only labels, i.e. {{example1}}, {{example2}}, or {{example3}}. Returns a list of time series associated with the entered data." }, "selector-actions": { "aria-label-selector": "selector", @@ -175,16 +175,16 @@ "label-scrape-interval": "Scrape interval", "label-series-limit": "Series limit", "label-use-series-endpoint": "Use series endpoint", - "more-info": "For more information on configuring prometheus type and version in data sources, see the <2>provisioning documentation.", + "more-info": "For more information on configuring prometheus type and version in data sources, see the <2>provisioning documentation .", "placeholder-example-maxsourceresolutionmtimeout": "Example: {{example}}", "title-interval-behaviour": "Interval behaviour", "title-other": "Other", "title-performance": "Performance", "title-query-editor": "Query editor", "tooltip-cache-level": "Sets the browser caching level for editor queries. Higher cache settings are recommended for high cardinality data sources.", - "tooltip-custom-query-parameters": "Add custom parameters to the Prometheus query URL. For example {{example1}}, {{example2}}, {{example3}}, or{{example4}}. Multiple parameters should be concatenated together with {{concatenationChar}}.", + "tooltip-custom-query-parameters": "Add custom parameters to the Prometheus query URL. For example {{example1}}, {{example2}}, {{example3}}, or {{example4}}. Multiple parameters should be concatenated together with {{concatenationChar}}.", "tooltip-default-editor": "Set default editor option for all users of this data source.", - "tooltip-disable-metrics-lookup": "Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. ", + "tooltip-disable-metrics-lookup": "Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances.", "tooltip-disable-recording-rules-beta": "This feature will disable recording rules. Turn this on to improve dashboard performance", "tooltip-http-method": "You can use either POST or GET HTTP method to query your Prometheus data source. POST is the recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network.", "tooltip-incremental-querying-beta": "This feature will change the default behavior of relative queries to always request fresh data from the prometheus instance, instead query results will be cached, and only new records are requested. Turn this on to decrease database and network load.", @@ -479,7 +479,7 @@ "aria-label-selector": "selector" }, "results-table": { - "content-descriptive-type": "When creating a {{descriptiveType}}, Prometheus exposes multiple series with the type counter. ", + "content-descriptive-type": "When creating a {{descriptiveType}}, Prometheus exposes multiple series with the type counter.", "description": "Description", "message-expand-label-filters": "There are no metrics found. Try to expand your label filters.", "message-expand-search": "There are no metrics found. Try to expand your search and filters.", diff --git a/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json index 8b0b9d4c5d1..80739214809 100644 --- a/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "La implementación original del editor de consultas de variables de Prometheus. Introduzca una cadena con el tipo de consulta y los parámetros correctos como se describe en estos documentos. Por ejemplo, {{exampleQuery}}.", "tooltip-label": "Devuelve una lista de valores de etiqueta para el nombre de etiqueta en todas las métricas, a menos que se especifique la métrica.", "tooltip-metric-regex": "Devuelve una lista de nombres de etiquetas, filtrando opcionalmente por la expresión regular de métrica especificada.", - "tooltip-query": "Devuelve una lista de resultados de la consulta de Prometheus para la consulta. Esto puede incluir funciones de Prometheus, es decir, {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "El plugin de fuente de datos de Prometheus proporciona los siguientes tipos de consulta para las variables de plantilla.", - "tooltip-series-query": "Introduzca una métrica con etiquetas, solo una métrica o solo etiquetas, es decir, {{example1}}, {{example2}} o {{example3}}. Devuelve una lista de series temporales asociadas con los datos introducidos." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "selector", @@ -175,16 +175,16 @@ "label-scrape-interval": "Intervalo de recuperación de datos", "label-series-limit": "Límite de series", "label-use-series-endpoint": "Utilizar punto de conexión de la serie", - "more-info": "Para obtener más información sobre cómo configurar el tipo y la versión de Prometheus en las fuentes de datos, consulte la <2>documentación de aprovisionamiento.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Ejemplo: {{example}}", "title-interval-behaviour": "Comportamiento del intervalo", "title-other": "Otro", "title-performance": "Rendimiento", "title-query-editor": "Editor de consultas", "tooltip-cache-level": "Establece el nivel de caché del navegador para las consultas del editor. Se recomienda una configuración de caché más alta para fuentes de datos de alta cardinalidad.", - "tooltip-custom-query-parameters": "Añada parámetros personalizados a la URL de la consulta de Prometheus. Por ejemplo, {{example1}}, {{example2}}, {{example3}} o{{example4}}. Deben concatenarse varios parámetros junto con {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Establece la opción de editor predeterminada para todos los usuarios de esta fuente de datos.", - "tooltip-disable-metrics-lookup": "Al marcar esta opción, se desactivará el selector de métricas y la compatibilidad con métricas/etiquetas en el autocompletado del campo de consulta. Esto es útil si tienes problemas de rendimiento con instancias de Prometheus de mayor tamaño. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Esta función deshabilitará las reglas de registro. Actívelo para mejorar el rendimiento del dashboard", "tooltip-http-method": "Puede utilizar el método HTTP POST o GET para consultar su fuente de datos de Prometheus. POST es el método recomendado, ya que permite consultas de mayor tamaño. Cámbielo a GET si tiene una versión de Prometheus anterior a la 2.1 o si las solicitudes POST están restringidas en su red.", "tooltip-incremental-querying-beta": "Esta función cambiará el comportamiento predeterminado de las consultas relativas para solicitar siempre datos nuevos de la instancia de Prometheus, en lugar de almacenar en caché los resultados de las consultas y solo se solicitarán nuevos registros. Actívelo para disminuir la carga de la base de datos y de la red.", @@ -479,7 +479,7 @@ "aria-label-selector": "selector" }, "results-table": { - "content-descriptive-type": "Al crear un {{descriptiveType}}, Prometheus expone varias series con el contador de tipos. ", + "content-descriptive-type": "", "description": "Descripción", "message-expand-label-filters": "No se han encontrado métricas. Intenta ampliar los filtros de etiquetas.", "message-expand-search": "No se han encontrado métricas. Intenta ampliar la búsqueda y los filtros.", diff --git a/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json index 2ad85dcac95..86169b30725 100644 --- a/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "L’implémentation d’origine de l’éditeur de requêtes à variables Prometheus. Saisissez une chaîne de caractères avec le type de requête et les paramètres appropriés comme décrit dans la documentation. Par exemple, {{exampleQuery}}.", "tooltip-label": "Renvoie une liste de valeurs d’étiquette pour le nom d’étiquette dans toutes les métriques, sauf si la métrique est spécifiée.", "tooltip-metric-regex": "Renvoie une liste de noms d’étiquettes, en filtrant éventuellement par expression régulière de métrique spécifiée.", - "tooltip-query": "Renvoie une liste de résultats de requête Prometheus pour la requête. Cela peut inclure des fonctions Prometheus, par exemple {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Le plugin de source de données Prometheus fournit les types de requêtes suivants pour les variables de modèle.", - "tooltip-series-query": "Saisissez une métrique avec des étiquettes, uniquement une métrique ou uniquement des étiquettes, c’est-à-dire {{example1}}, {{example2}} ou {{example3}}. Renvoie une liste de séries chronologiques associées aux données saisies." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "sélecteur", @@ -175,16 +175,16 @@ "label-scrape-interval": "Intervalle de scraping", "label-series-limit": "Limite de séries", "label-use-series-endpoint": "Utiliser le point de terminaison de la série", - "more-info": "Pour en savoir plus sur la configuration du type et de la version de Prometheus dans les sources de données, consultez la <2>documentation sur le provisionnement.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Exemple : {{example}}", "title-interval-behaviour": "Comportement de l’intervalle", "title-other": "Autre", "title-performance": "Performance", "title-query-editor": "Éditeur de requêtes", "tooltip-cache-level": "Définit le niveau de mise en cache du navigateur pour les requêtes de l’éditeur. Des paramètres de cache plus élevés sont recommandés pour les sources de données à cardinalité élevée.", - "tooltip-custom-query-parameters": "Ajoutez des paramètres personnalisés à l’URL de la requête Prometheus. Par exemple {{example1}}, {{example2}}, {{example3}}, ou {{example4}}. Plusieurs paramètres doivent être concaténés avec {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Définir l’option d’éditeur par défaut pour tous les utilisateurs de cette source de données.", - "tooltip-disable-metrics-lookup": "Cocher cette option désactivera le sélecteur de métriques et la prise en charge des métriques/étiquettes dans la saisie semi-automatique du champ de requête. Cela vous aide si vous avez des problèmes de performances avec des instances Prometheus plus grandes. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Cette fonctionnalité désactivera les règles d’enregistrement. Activez cette option pour améliorer les performances du tableau de bord", "tooltip-http-method": "Vous pouvez utiliser la méthode POST ou GET HTTP pour interroger votre source de données Prometheus. POST est la méthode recommandée car elle permet des requêtes plus volumineuses. Changez ceci en GET si vous avez une version Prometheus antérieure à la version 2.1 ou si les demandes POST sont restreintes dans votre réseau.", "tooltip-incremental-querying-beta": "Cette fonctionnalité modifiera le comportement par défaut des requêtes relatives pour toujours demander des données récentes à l’instance Prometheus. Au lieu de cela, les résultats des requêtes seront mis en cache et seuls les nouveaux enregistrements seront demandés. Activez cette option pour réduire la charge de la base de données et du réseau.", @@ -479,7 +479,7 @@ "aria-label-selector": "sélecteur" }, "results-table": { - "content-descriptive-type": "Lors de la création d’un(e) {{descriptiveType}}, Prometheus expose plusieurs séries avec le compteur de type. ", + "content-descriptive-type": "", "description": "Description", "message-expand-label-filters": "Aucune métrique trouvée. Essayez d’élargir vos filtres d’étiquettes.", "message-expand-search": "Aucune métrique trouvée. Essayez d’élargir votre recherche et vos filtres.", diff --git a/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json index 5a8d1906a38..bceeabefdc1 100644 --- a/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "A Prometheus változólekérdezés-szerkesztő eredeti implementációja. Adjon meg egy karakterláncot a megfelelő lekérdezéstípussal és paraméterekkel, a jelen dokumentumokban leírtak szerint. Például: {{exampleQuery}}.", "tooltip-label": "A címke nevéhez tartozó címkeértékek listáját adja vissza az összes metrikában, kivéve, ha a metrika meg van adva.", "tooltip-metric-regex": "A címkenevek listáját adja vissza, opcionálisan a megadott metrikai reguláris kifejezés szerint szűrve.", - "tooltip-query": "A lekérdezéshez a Prometheus-lekérdezési eredmények listáját adja vissza. Ez magában foglalhatja a Prometheus-funkciókat, azaz {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "A Prometheus adatforrás-beépülőmodul a következő lekérdezéstípusokat biztosítja a sablonváltozókhoz.", - "tooltip-series-query": "Adjon meg egy metrikát címkékkel, csak egy metrikát vagy csak címkéket: {{example1}}, {{example2}} vagy {{example3}}. A bevitt adatokhoz társított idősorok listáját adja vissza." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "választó", @@ -175,16 +175,16 @@ "label-scrape-interval": "Adatgyűjtési intervallum", "label-series-limit": "Sorozatkorlát", "label-use-series-endpoint": "Sorozat végpontjának használata", - "more-info": "A Prometheus-típus és -verzió adatforrásokban történő konfigurálásával kapcsolatos további információkért tekintse meg a <2>kiépítési dokumentációt.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Példa: {{example}}", "title-interval-behaviour": "Intervallum viselkedése", "title-other": "Egyéb", "title-performance": "Teljesítmény", "title-query-editor": "Lekérdezésszerkesztő", "tooltip-cache-level": "Beállítja a böngésző gyorsítótárának szintjét a szerkesztői lekérdezésekhez. A magasabb számossági adatforrásokhoz magasabb gyorsítótár-beállítások ajánlottak.", - "tooltip-custom-query-parameters": "Egyéni paraméterek hozzáadása a Prometheus lekérdezési URL-jéhez. Például: {{example1}}, {{example2}}, {{example3}} vagy {{example4}}. Több paramétert a következővel kell összefűzni: {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Beállítja az alapértelmezett szerkesztőopciót az adatforrás minden felhasználója számára.", - "tooltip-disable-metrics-lookup": "Ennek az opciónak a bejelölése letiltja a metrikaválasztót és a metrika-/címketámogatást a lekérdezési mező automatikus kitöltésében. Ez segít, ha a nagyobb Prometheus-példányokkal teljesítményproblémái vannak. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Ez a funkció letiltja a felvételkészítési szabályokat. Kapcsolja be az irányítópult teljesítményének javítása érdekében", "tooltip-http-method": "A Prometheus-adatforrás lekérdezéséhez használhatja a POST- vagy a GET HTTP-metódust. A POST az ajánlott metódus, mivel nagyobb lekérdezéseket tesz lehetővé. Módosítsa ezt GET-re, ha a Prometheus-verziója régebbi, mint a 2.1-es, vagy ha a POST-kérések korlátozva vannak a hálózatában.", "tooltip-incremental-querying-beta": "Ez a funkció megváltoztatja a relatív lekérdezések azon alapértelmezett viselkedését, hogy mindig friss adatokat kérjenek le a Prometheus-példányból, ehelyett a lekérdezési eredményeket a rendszer gyorsítótárazza, és csak új rekordok lesznek lekérve. Kapcsolja be az adatbázis és a hálózat terhelésének csökkentése érdekében.", @@ -479,7 +479,7 @@ "aria-label-selector": "választó" }, "results-table": { - "content-descriptive-type": "{{descriptiveType}} létrehozásakor a Prometheus több, számláló típusú sorozatot jelenít meg. ", + "content-descriptive-type": "", "description": "Leírás", "message-expand-label-filters": "Nem található metrika. Próbálja meg kibővíteni a címkeszűrőket.", "message-expand-search": "Nem található metrika. Próbálja meg kibővíteni a keresést és a szűrőket.", diff --git a/packages/grafana-prometheus/src/locales/i18next-parser.config.cjs b/packages/grafana-prometheus/src/locales/i18next-parser.config.cjs deleted file mode 100644 index 2e3161f3761..00000000000 --- a/packages/grafana-prometheus/src/locales/i18next-parser.config.cjs +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages - sort: true, - createOldCatalogs: false, - failOnWarnings: true, - verbose: false, - resetDefaultValueLocale: 'en-US', // Updates extracted values when they change in code - - defaultNamespace: 'grafana-prometheus', - input: ['../**/*.{tsx,ts}'], - output: './src/locales/$LOCALE/$NAMESPACE.json', -}; diff --git a/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json index cf37adca5c0..74b9ed50d74 100644 --- a/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Implementasi awal dari editor kueri variabel Prometheus. Masukkan string dengan jenis kueri dan parameter yang benar seperti yang dijelaskan dalam dokumen ini. Misalnya, {{exampleQuery}}.", "tooltip-label": "Mengembalikan daftar nilai label untuk nama label di semua metrik kecuali metrik tersebut ditentukan.", "tooltip-metric-regex": "Mengembalikan daftar nama label, secara opsional menyaring berdasarkan regex metrik yang ditentukan.", - "tooltip-query": "Mengembalikan daftar hasil kueri Prometheus untuk kueri tersebut. Ini dapat mencakup fungsi Prometheus, yaitu {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Plugin sumber data Prometheus menyediakan jenis kueri berikut untuk variabel templat.", - "tooltip-series-query": "Masukkan metrik dengan label, hanya metrik atau hanya label, yaitu {{example1}}, {{example2}}, atau {{example3}}. Mengembalikan daftar deret waktu yang terkait dengan data yang dimasukkan." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "selektor", @@ -175,16 +175,16 @@ "label-scrape-interval": "Interval scrape", "label-series-limit": "Batas data seri", "label-use-series-endpoint": "Gunakan titik akhir data seri", - "more-info": "Untuk informasi lengkap tentang mengonfigurasi jenis dan versi prometheus dalam sumber data, lihat <2>dokumentasi penyediaan.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Contoh: {{example}}", "title-interval-behaviour": "Perilaku interval", "title-other": "Lainnya", "title-performance": "Kinerja", "title-query-editor": "Editor kueri", "tooltip-cache-level": "Mengatur level cache peramban untuk kueri editor. Pengaturan cache yang lebih tinggi disarankan untuk sumber data dengan kardinalitas tinggi.", - "tooltip-custom-query-parameters": "Tambahkan parameter kustom ke URL kueri Prometheus. Misalnya {{example1}}, {{example2}}, {{example3}}, atau {{example4}}. Beberapa parameter harus digabungkan bersama dengan {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Atur opsi editor default untuk semua pengguna sumber data ini.", - "tooltip-disable-metrics-lookup": "Memeriksa opsi ini akan menonaktifkan pemilih metrik dan dukungan metrik/label dalam pelengkapan otomatis bidang kueri. Ini membantu jika Anda memiliki masalah kinerja dengan instans Prometheus yang lebih besar. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Fitur ini akan menonaktifkan aturan perekaman. Aktifkan ini untuk meningkatkan kinerja dasbor", "tooltip-http-method": "Anda dapat menggunakan metode HTTP POST atau GET untuk melakukan kueri sumber data Prometheus Anda. POST adalah metode yang disarankan karena memungkinkan kueri yang lebih besar. Ubah ini menjadi GET jika Anda memiliki versi Prometheus yang lebih lama dari 2.1 atau jika permintaan POST dibatasi di jaringan Anda.", "tooltip-incremental-querying-beta": "Fitur ini akan mengubah perilaku default kueri relatif: alih-alih selalu meminta data terbaru dari instans Prometheus, hasil kueri akan disimpan sementara, dan hanya data baru yang akan diminta. Aktifkan ini untuk mengurangi beban basis data dan jaringan.", @@ -479,7 +479,7 @@ "aria-label-selector": "selektor" }, "results-table": { - "content-descriptive-type": "Saat membuat {{descriptiveType}}, Prometheus mengekspos beberapa data seri dengan penghitung jenis. ", + "content-descriptive-type": "", "description": "Deskripsi", "message-expand-label-filters": "Tidak ada metrik yang ditemukan. Coba perluas filter label Anda.", "message-expand-search": "Tidak ada metrik yang ditemukan. Coba perluas pencarian dan filter Anda.", diff --git a/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json index 314ac50298e..f3cf622ddd1 100644 --- a/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "L'implementazione originale dell'editor di query variabile Prometheus. Inserisci una stringa con il tipo di query e i parametri corretti come descritto in questi documenti. Ad esempio, {{exampleQuery}}.", "tooltip-label": "Restituisce un elenco di valori di etichetta per il nome dell'etichetta in tutte le metriche, a meno che la metrica non sia specificata.", "tooltip-metric-regex": "Restituisce un elenco di nomi di etichette, filtrando facoltativamente in base all'espressione regolare metrica specificata.", - "tooltip-query": "Restituisce un elenco di risultati della query Prometheus per la query. Questo può includere funzioni Prometheus, ad esempio {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Il plugin dell'origine dati di Prometheus fornisce i seguenti tipi di query per le variabili del modello.", - "tooltip-series-query": "Inserisci una metrica con etichette, solo una metrica o solo etichette, ad esempio {{example1}}, {{example2}} o {{example3}}. Restituisce un elenco di serie temporali associate ai dati inseriti." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "selettore", @@ -175,16 +175,16 @@ "label-scrape-interval": "Intervallo di raccolta", "label-series-limit": "Limite della serie", "label-use-series-endpoint": "Usa endpoint della serie", - "more-info": "Per ulteriori informazioni sulla configurazione del tipo e della versione di Prometheus nelle origini dati, consulta la <2>documentazione sul provisioning.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Esempio: {{example}}", "title-interval-behaviour": "Comportamento intervallo", "title-other": "Altro", "title-performance": "Performance", "title-query-editor": "Editor query", "tooltip-cache-level": "Imposta il livello di memorizzazione nella cache del browser per le query dell'editor. Si consigliano impostazioni della cache più elevate per origini dati ad alta cardinalità.", - "tooltip-custom-query-parameters": "Aggiungi parametri personalizzati all'URL della query Prometheus. Ad esempio {{example1}}, {{example2}}, {{example3}} o {{example4}}. Più parametri devono essere concatenati insieme a {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Imposta l'opzione editor predefinita per tutti gli utenti di questa origine dati.", - "tooltip-disable-metrics-lookup": "Selezionando questa opzione, il selettore di metriche e il supporto di metriche/etichette nel completamento automatico del campo di query verranno disabilitati. Questo è utile se hai problemi di prestazioni con istanze Prometheus più grandi. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Questa funzionalità disabiliterà le regole di registrazione. Attiva questa opzione per migliorare le prestazioni della dashboard", "tooltip-http-method": "Puoi utilizzare il metodo POST o GET HTTP per eseguire query sull'origine dati di Prometheus. POST è il metodo consigliato in quanto consente query più grandi. Usa GET se hai una versione di Prometheus precedente alla 2.1 o se le richieste POST sono limitate nella tua rete.", "tooltip-incremental-querying-beta": "Questa funzionalità modificherà il comportamento predefinito delle query relative per richiedere sempre dati aggiornati dall'istanza di Prometheus, mentre i risultati delle query verranno memorizzati nella cache e verranno richiesti solo i nuovi record. Attiva questa opzione per ridurre il carico del database e della rete.", @@ -479,7 +479,7 @@ "aria-label-selector": "selettore" }, "results-table": { - "content-descriptive-type": "Quando si crea un {{descriptiveType}}, Prometheus mostra più serie con il contatore di tipo. ", + "content-descriptive-type": "", "description": "Descrizione", "message-expand-label-filters": "Non sono state trovate metriche. Prova ad espandere i filtri delle etichette.", "message-expand-search": "Non sono state trovate metriche. Prova ad espandere la ricerca e i filtri.", diff --git a/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json index 9efa150bca3..8f6fcef6ab6 100644 --- a/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Prometheus変数クエリエディターの元の実装。これらのドキュメントに記載されているように、正しいクエリタイプとパラメーターを含む文字列を入力してください。例: {{exampleQuery}}。", "tooltip-label": "メトリックが指定されていない限り、すべてのメトリックのラベル名に対するラベル値のリストを返します。", "tooltip-metric-regex": "ラベル名のリストを返します。オプションで、指定されたメトリック正規表現でフィルタリングすることも可能です。", - "tooltip-query": "クエリのPrometheusクエリ結果のリストを返します。これには、Prometheus関数を含めることもできます。例: {{exampleQuery}}。", + "tooltip-query": "", "tooltip-query-type": "Prometheusデータソースプラグインは、テンプレート変数に以下のクエリタイプを入力します。", - "tooltip-series-query": "ラベル付きメトリック、メトリックのみ、またはラベルのみを入力します。例: {{example1}}、{{example2}}、または{{example3}}。入力されたデータに関連する時系列のリストを返します。" + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "セレクター", @@ -175,16 +175,16 @@ "label-scrape-interval": "スクレイプ間隔", "label-series-limit": "系列制限", "label-use-series-endpoint": "系列エンドポイントを使用", - "more-info": "データソースでのPrometheusタイプとバージョンの設定の詳細については、<2>プロビジョニングドキュメントをご覧ください。", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "例: {{example}}", "title-interval-behaviour": "間隔の動作", "title-other": "その他", "title-performance": "パフォーマンス", "title-query-editor": "クエリエディター", "tooltip-cache-level": "エディタークエリのブラウザーキャッシュレベルを設定します。高カーディナリティデータソースには、より高いキャッシュ設定を推奨します。", - "tooltip-custom-query-parameters": "PrometheusクエリURLにカスタムパラメーターを追加します。例: {{example1}}、{{example2}}、{{example3}}、または{{example4}}。複数のパラメーターは{{concatenationChar}}で連結する必要があります。", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "このデータソースのすべてのユーザーにデフォルトエディターオプションを設定します。", - "tooltip-disable-metrics-lookup": "このオプションをオンにすると、クエリフィールドのオートコンプリートでメトリック選択とメトリック/ラベルサポートが無効になります。これは、大規模なPrometheusインスタンスをご利用でパフォーマンスの問題がある場合に役立ちます。", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "この機能は記録ルールを無効にします。ダッシュボードのパフォーマンスを向上させるには、この機能をオンにしてください", "tooltip-http-method": "PrometheusデータソースをクエリするにはPOSTまたはGET HTTPメソッドのいずれかを使用できます。POSTメソッドだと、より大きなクエリを実行できるため、推奨される方法です。Prometheusのバージョンが2.1より古い場合、またはネットワークでPOSTリクエストが制限されている場合は、これをGETに変更してください。", "tooltip-incremental-querying-beta": "この機能は相対クエリのデフォルト動作が変更され、常にPrometheusインスタンスから新しいデータをリクエストする代わりに、クエリ結果をキャッシュして新しいレコードのみをリクエストするようになります。データベースとネットワークの負荷を軽減するには、これをオンにしてください。", @@ -479,7 +479,7 @@ "aria-label-selector": "セレクター" }, "results-table": { - "content-descriptive-type": "{{descriptiveType}}を作成する際、Prometheusはタイプカウンターで複数の系列を公開します。", + "content-descriptive-type": "", "description": "説明", "message-expand-label-filters": "メトリックが見つかりません。ラベルフィルターを拡張してみてください。", "message-expand-search": "メトリックが見つかりません。検索とフィルターを拡張してみてください。", diff --git a/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json index 857372ed06e..60576a8c7b6 100644 --- a/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Prometheus 변수 쿼리 편집기의 원래 구현입니다. 이 문서에 설명된 대로 올바른 쿼리 유형 및 매개변수가 있는 문자열을 입력합니다. 예: {{exampleQuery}}.", "tooltip-label": "메트릭이 지정되지 않은 한, 모든 메트릭에서 레이블 이름에 대한 레이블 값 목록을 반환합니다.", "tooltip-metric-regex": "필요에 따라 지정된 메트릭 정규식을 기준으로 필터링하여, 레이블 이름 목록을 반환합니다.", - "tooltip-query": "쿼리에 대한 Prometheus 쿼리 결과 목록을 반환합니다. 여기에는 Prometheus 기능이 포함될 수 있습니다. 예: {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Prometheus 데이터 소스 플러그인은 템플릿 변수에 대해 다음과 같은 쿼리 유형을 제공합니다.", - "tooltip-series-query": "레이블이 있는 메트릭 또는 메트릭만 단독으로 또는 레이블만 단독으로 입력하세요. 예: {{example1}}, {{example2}}, {{example3}}. 입력된 데이터와 관련된 시계열 목록을 반환합니다." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "선택기", @@ -175,16 +175,16 @@ "label-scrape-interval": "스크레이핑 간격", "label-series-limit": "계열 한도", "label-use-series-endpoint": "계열 엔드포인트 사용", - "more-info": "데이터 소스에서 prometheus 유형 및 버전을 구성하는 방법에 대한 자세한 내용은 <2>프로비저닝 문서를 참조하시기 바랍니다.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "예: {{example}} ", "title-interval-behaviour": "간격 동작", "title-other": "기타", "title-performance": "성능", "title-query-editor": "쿼리 편집기", "tooltip-cache-level": "편집기 쿼리에 대한 브라우저 캐싱 수준을 설정합니다. 카디널리티(cardinality)가 높은 데이터 소스에는 더 높은 캐시 설정을 권장합니다.", - "tooltip-custom-query-parameters": "Prometheus 쿼리 URL에 사용자 지정 매개변수를 추가합니다. 예: {{example1}}, {{example2}}, {{example3}}, {{example4}}. 여러 파라미터는 {{concatenationChar}}와(과) 함께 연결해야 합니다.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "이 데이터 소스의 모든 사용자에 대한 기본 편집기 옵션을 설정합니다.", - "tooltip-disable-metrics-lookup": "이 옵션을 선택하면, 쿼리 필드의 자동 완성에서 메트릭 선택기 및 메트릭/레이블 지원이 비활성화됩니다. 이는 대규모 Prometheus 인스턴스에서 성능 문제가 발생하는 경우에 유용합니다. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "이 기능을 사용하면 기록 규칙이 비활성화됩니다. 대시보드 성능을 향상시키려면 이 기능을 켜세요.", "tooltip-http-method": "POST 또는 GET HTTP 메소드를 사용하여 Prometheus 데이터 소스를 쿼리할 수 있습니다. POST는 더 큰 쿼리를 허용하기 때문에 권장되는 메소드입니다. Prometheus 버전이 2.1보다 이전 버전이거나 네트워크에서 POST 요청이 제한되어 있는 경우 이를 GET으로 변경하세요.", "tooltip-incremental-querying-beta": "이 기능은 상대적 쿼리의 기본 동작을 항상 Prometheus 인스턴스에서 최신 데이터를 요청하도록 변경합니다. 대신 쿼리 결과가 캐시되고 새 레코드만 요청됩니다. 데이터베이스 및 네트워크 부하를 줄이려면 이 기능을 켜세요.", @@ -479,7 +479,7 @@ "aria-label-selector": "선택기" }, "results-table": { - "content-descriptive-type": "{{descriptiveType}}을 생성할 때 Prometheus는 유형 카운터와 함께 여러 계열을 노출합니다. ", + "content-descriptive-type": "", "description": "설명", "message-expand-label-filters": "메트릭을 찾을 수 없습니다. 레이블 필터를 확장해 보세요.", "message-expand-search": "메트릭을 찾을 수 없습니다. 검색 및 필터를 확장해 보세요.", diff --git a/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json index 991614b0524..feaca3639d9 100644 --- a/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "De oorspronkelijke implementatie van de Prometheus-variabele queryeditor. Voer een tekenreeks in met het juiste querytype en de juiste parameters zoals beschreven in deze documenten. Bijvoorbeeld, {{exampleQuery}}.", "tooltip-label": "Retourneert een lijst met labelwaarden voor de labelnaam in alle metriek, tenzij de metriek is opgegeven.", "tooltip-metric-regex": "Retourneert een lijst met labelnamen, optioneel gefilterd op opgegeven metriekregex.", - "tooltip-query": "Retourneert een lijst met Prometheus-zoekresultaten voor de zoekopdracht. Dit kan Prometheus-functies omvatten, bijv. {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "De Prometheus-gegevensbronplugin biedt de volgende querytypes voor sjabloonvariabelen.", - "tooltip-series-query": "Voer een metriek in met labels, alleen een metriek of alleen labels, bijv. {{example1}}, {{example2}} of {{example3}}. Retourneert een lijst met tijdreeksen die zijn gekoppeld aan de ingevoerde gegevens." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "kiezer", @@ -175,16 +175,16 @@ "label-scrape-interval": "Scrape-interval", "label-series-limit": "Reekslimiet", "label-use-series-endpoint": "Serie-eindpunt gebruiken", - "more-info": "Zie de <2>provisioning-documentatie voor meer informatie over het configureren van het Prometheus-type en de versie in gegevensbronnen.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Voorbeeld: {{example}}", "title-interval-behaviour": "Intervalgedrag", "title-other": "Overige", "title-performance": "Prestaties", "title-query-editor": "Query-editor", "tooltip-cache-level": "Stelt het cacheniveau van de browser in voor editor-query's. Hogere cache-instellingen worden aanbevolen voor gegevensbronnen met een hoge kardinaliteit.", - "tooltip-custom-query-parameters": "Voeg aangepaste parameters toe aan de Prometheus-query-URL. Bijvoorbeeld {{example1}}, {{example2}}, {{example3}}, of{{example4}}. Meerdere parameters moeten worden samengevoegd met {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Stel de standaard editoroptie in voor alle gebruikers van deze gegevensbron.", - "tooltip-disable-metrics-lookup": "Als je deze optie aanvinkt, worden de metriekkiezer en de metriek/label-ondersteuning in de auto-aanvulling van het query-veld uitgeschakeld. Dit helpt als je prestatieproblemen hebt met grotere Prometheus-instanties. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Deze functie schakelt opnameregels uit. Schakel dit in om de prestaties van het dashboard te verbeteren", "tooltip-http-method": "Je kunt de HTTP-methode POST of GET gebruiken om je Prometheus-gegevensbron op te vragen. POST is de aanbevolen methode omdat het grotere query's toestaat. Wijzig dit in GET als je een Prometheus-versie ouder dan 2.1 hebt of als POST-verzoeken in je netwerk zijn beperkt.", "tooltip-incremental-querying-beta": "Deze functie zal het standaardgedrag van relatieve query's veranderen om altijd nieuwe gegevens van de Prometheus-instantie op te vragen, in plaats daarvan worden query-resultaten in de cache opgeslagen en worden alleen nieuwe records opgevraagd. Schakel dit in om de database- en netwerkbelasting te verminderen.", @@ -479,7 +479,7 @@ "aria-label-selector": "kiezer" }, "results-table": { - "content-descriptive-type": "Bij het maken van een {{descriptiveType}} stelt Prometheus meerdere series bloot met de typeteller. ", + "content-descriptive-type": "", "description": "Beschrijving", "message-expand-label-filters": "Er zijn geen statistieken gevonden. Probeer uw labelfilters uit te breiden.", "message-expand-search": "Er zijn geen statistieken gevonden. Probeer uw zoekopdracht en filters uit te breiden.", diff --git a/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json index 3c949a12c2f..f0b3e694062 100644 --- a/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Oryginalna implementacja edytora zapytań zmiennych w systemie Prometheus. Wpisz ciąg z prawidłowym typem zapytania i parametrami zgodnie z opisem w tych dokumentach. Przykład: {{exampleQuery}}.", "tooltip-label": "Zwraca listę wartości etykiet dla nazwy etykiety we wszystkich wskaźnikach, chyba że wskaźnik został określony.", "tooltip-metric-regex": "Zwraca listę nazw etykiet, opcjonalnie filtrując ją według określonego wyrażenia regularnego wskaźnika.", - "tooltip-query": "Zwraca listę wyników zapytania Prometheus dla zapytania. Może to obejmować funkcje systemu Prometheus, czyli {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Wtyczka źródła danych Prometheus zapewnia następujące typy zapytań dla zmiennych szablonu.", - "tooltip-series-query": "Wprowadź wskaźnik z etykietami, sam wskaźnik lub same etykiety, czyli {{example1}}, {{example2}} lub {{example3}}. Zwraca listę szeregów czasowych powiązanych z wprowadzonymi danymi." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "selektor", @@ -175,16 +175,16 @@ "label-scrape-interval": "Odstęp czasowy pobierania danych", "label-series-limit": "Limit szeregu", "label-use-series-endpoint": "Użyj punktu końcowego szeregu", - "more-info": "Więcej informacji na temat konfigurowania typu i wersji systemu Prometheus w źródłach danych można znaleźć w <2>dokumentach dotyczących aprowizacji.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Przykład: {{example}}", "title-interval-behaviour": "Sposób działania odstępu czasu", "title-other": "Inne", "title-performance": "Wydajność", "title-query-editor": "Edytor zapytań", "tooltip-cache-level": "Ustawia poziom buforowania przeglądarki dla zapytań edytora. W przypadku źródeł danych o wysokiej kardynalności zaleca się ustawienie pamięci podręcznej na wyższym poziomie.", - "tooltip-custom-query-parameters": "Dodaj niestandardowe parametry do adresu URL zapytania Prometheus. Na przykład {{example1}}, {{example2}}, {{example3}} lub {{example4}}. Wiele parametrów należy połączyć razem za pomocą znaków {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Ustaw domyślną opcję edytora dla wszystkich użytkowników tego źródła danych.", - "tooltip-disable-metrics-lookup": "Zaznaczenie tej opcji spowoduje wyłączenie selektora wskaźników i obsługi wskaźników/etykiet w autouzupełnianiu pola zapytania. Pomaga to w przypadku problemów z wydajnością większych instancji systemu Prometheus. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Ta funkcja wyłącza reguły rejestrowania. Włącz tę opcję, aby poprawić wydajność pulpitu", "tooltip-http-method": "Możesz użyć metody HTTP POST lub GET, aby wysłać zapytanie do źródła danych Prometheus. POST jest zalecaną metodą, ponieważ obsługuje większe zapytania. Wybierz metodę GET, jeśli wersja systemu Prometheus jest starsza niż 2.1 lub jeśli żądania POST są objęte ograniczeniami w Twojej sieci.", "tooltip-incremental-querying-beta": "Ta funkcja zmienia domyślny sposób działania zapytań względnych, aby zawsze żądały świeżych danych z instancji Prometheus. Zamiast tego wyniki zapytań będą buforowane, a wysyłane żądania będą uwzględniać tylko nowe rekordy. Włącz tę opcję, aby zmniejszyć obciążenie bazy danych i sieci.", @@ -479,7 +479,7 @@ "aria-label-selector": "selektor" }, "results-table": { - "content-descriptive-type": "Podczas tworzenia zapytania {{descriptiveType}} system Prometheus udostępnia wiele szeregów z licznikiem typu. ", + "content-descriptive-type": "", "description": "Opis", "message-expand-label-filters": "Nie znaleziono żadnych metryk. Spróbuj rozszerzyć filtry etykiet.", "message-expand-search": "Nie znaleziono żadnych metryk. Spróbuj rozszerzyć wyszukiwanie i filtry.", diff --git a/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json index 74d47a5342b..07598624e45 100644 --- a/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "A implementação original do editor de consultas variáveis do Prometheus. Insira uma string com o tipo de consulta e os parâmetros corretos, conforme descrito nesta documentação. Por exemplo, {{exampleQuery}}.", "tooltip-label": "Retorna uma lista de valores de rótulo para o nome do rótulo em todas as métricas, a menos que a métrica seja especificada.", "tooltip-metric-regex": "Retorna uma lista de nomes de rótulos, havendo a opção de filtrar por métrica regex especificada.", - "tooltip-query": "Retorna uma lista de resultados de consulta do Prometheus para a consulta. Isso pode incluir funções do Prometheus, como {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "O plug-in de fonte de dados do Prometheus fornece os seguintes tipos de consulta para variáveis de modelo.", - "tooltip-series-query": "Insira uma métrica com rótulos, apenas uma métrica ou apenas rótulos, como {{example1}}, {{example2}} ou {{example3}}. Retorna uma lista de séries temporais associadas aos dados inseridos." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "seletor", @@ -175,16 +175,16 @@ "label-scrape-interval": "Intervalo de coleta", "label-series-limit": "Limite de série", "label-use-series-endpoint": "Usar endpoint da série", - "more-info": "Para mais informações sobre como configurar o tipo e a versão do Prometheus em fontes de dados, consulte a <2>documentação de provisionamento.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Exemplo: {{example}}", "title-interval-behaviour": "Comportamento de intervalo", "title-other": "Outro", "title-performance": "Desempenho", "title-query-editor": "Editor de consultas", "tooltip-cache-level": "Define o nível de cache do navegador para consultas do editor. Recomenda-se usar configurações de cache mais elevadas para fontes de dados de alta cardinalidade.", - "tooltip-custom-query-parameters": "Adicione parâmetros personalizados ao URL de consulta do Prometheus. Por exemplo, {{example1}}, {{example2}}, {{example3}} ou{{example4}}. Vários parâmetros devem ser concatenados com {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Defina a opção de editor padrão para todos os usuários desta fonte de dados.", - "tooltip-disable-metrics-lookup": "Marcar esta opção desativará o seletor de métricas e a compatibilidade com métricas/rótulos no preenchimento automático do campo de consulta. Isso é útil se você tiver problemas de desempenho com instâncias maiores do Prometheus. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Este recurso desativará as regras de registro. Ative o recurso para aprimorar o desempenho do painel", "tooltip-http-method": "Você pode usar o método HTTP POST ou GET para consultar sua fonte de dados do Prometheus. POST é o método recomendado por permitir consultas maiores. Altere para GET se sua versão do Prometheus for anterior à versão 2.1 ou se as solicitações POST estiverem restritas na sua rede.", "tooltip-incremental-querying-beta": "Este recurso alterará o comportamento padrão das consultas relativas: os dados recentes deixam de ser sempre solicitados da instância do Prometheus e passam a utilizar dados em cache e a solicitar apenas novos registros. Ative o recurso para diminuir a carga do banco de dados e da rede.", @@ -479,7 +479,7 @@ "aria-label-selector": "seletor" }, "results-table": { - "content-descriptive-type": "Ao criar um {{descriptiveType}}, o Prometheus expõe várias séries com o contador de tipos. ", + "content-descriptive-type": "", "description": "Descrição", "message-expand-label-filters": "Não há métricas encontradas. Tente expandir seus filtros de rótulo.", "message-expand-search": "Não há métricas encontradas. Tente expandir sua busca e filtros.", diff --git a/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json index 0c56b656f66..eb90445ed3e 100644 --- a/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "A implementação original do editor de consultas de variáveis Prometheus. Introduzir uma cadeia de carateres com o tipo de consulta e parâmetros corretos, conforme descrito nestes documentos. Por exemplo, {{exampleQuery}}.", "tooltip-label": "Devolve uma lista de valores de etiqueta para o nome da etiqueta em todas as métricas, a menos que a métrica seja especificada.", "tooltip-metric-regex": "Devolve uma lista de nomes de etiquetas, filtrando opcionalmente por regex métrico especificado.", - "tooltip-query": "Devolve uma lista de resultados de consulta Prometheus para a consulta. Isto pode incluir funções Prometheus, ou seja,{{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "O plugin de origem de dados do Prometheus fornece os seguintes tipos de consulta para variáveis de modelo.", - "tooltip-series-query": "Introduza uma métrica com etiquetas, apenas uma métrica ou apenas etiquetas, ou seja, {{example1}}, {{example2}}, ou {{example3}}. Devolve uma lista de séries temporais associadas aos dados inseridos." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "seletor", @@ -175,16 +175,16 @@ "label-scrape-interval": "Intervalo de raspagem", "label-series-limit": "Limite de série", "label-use-series-endpoint": "Utilizar ponto final da série", - "more-info": "Para obter mais informações sobre como configurar o tipo e a versão do Prometheus em origens de dados, consulte a <2>documentação de aprovisionamento.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Exemplo: {{example}}", "title-interval-behaviour": "Comportamento de intervalo", "title-other": "Outro", "title-performance": "Desempenho", "title-query-editor": "Editor de consultas", "tooltip-cache-level": "Definir o nível de cache do navegador para consultas do editor. São recomendadas definições de cache mais elevadas para origens de dados de elevada cardinalidade.", - "tooltip-custom-query-parameters": "Adicionar parâmetros personalizados ao URL de consulta do Prometheus. Por exemplo, {{example1}}, {{example2}}, {{example3}} ou {{example4}}. Os parâmetros múltiplos devem ser concatenados com {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Definir a opção de editor predefinida para todos os utilizadores desta origem de dados.", - "tooltip-disable-metrics-lookup": "Ao selecionar esta opção desativará o seletor de métricas e o suporte de métrica/etiqueta no preenchimento automático do campo de consulta. Isto ajuda se tiver problemas de desempenho com instâncias maiores do Prometheus. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Esta funcionalidade desativará as regras de gravação. Ative isto para melhorar o desempenho do painel de controlo", "tooltip-http-method": "Pode utilizar o método HTTP POST ou GET para consultar a sua origem de dados Prometheus. POST é o método recomendado, pois permite consultas maiores. Altere esta opção para GET se tiver uma versão do Prometheus anterior à 2.1 ou se os pedidos POST forem restritos na sua rede.", "tooltip-incremental-querying-beta": "Esta funcionalidade irá alterar o comportamento predefinido das consultas relativas para solicitar sempre dados novos da instância do Prometheus. Em vez disso, os resultados da consulta serão armazenados em cache e apenas serão solicitados novos registos. Ativar isto para diminuir a carga da base de dados e da rede.", @@ -479,7 +479,7 @@ "aria-label-selector": "seletor" }, "results-table": { - "content-descriptive-type": "Ao criar um {{descriptiveType}}, o Prometheus expõe várias séries com o contador de tipos. ", + "content-descriptive-type": "", "description": "Descrição", "message-expand-label-filters": "Não foram encontradas métricas. Tente expandir os seus filtros de etiquetas.", "message-expand-search": "Não foram encontradas métricas. Tente expandir a sua pesquisa e filtros.", diff --git a/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json index da73342e2e9..e4ff771a093 100644 --- a/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Оригинальная реализация редактора запросов переменных Prometheus. Введите строку с правильным типом запроса и параметрами, указанными в документации. Например, {{exampleQuery}}.", "tooltip-label": "Возвращает список значений для метки с соответствующим названием во всех метриках, если не указана конкретная метрика.", "tooltip-metric-regex": "Возвращает список названий меток, при необходимости фильтруя их по указанному регулярному выражению метрики.", - "tooltip-query": "Возвращает список результатов запросов Prometheus для текущего запроса. Сюда могут входить функции Prometheus, например {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Плагин источника данных Prometheus предоставляет следующие типы запросов для переменных шаблона.", - "tooltip-series-query": "Укажите метрику с метками, только метрику или только метки, например {{example1}}, {{example2}} или {{example3}}. Возвращает список временных рядов, связанных с введенными данными." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "селектор", @@ -175,16 +175,16 @@ "label-scrape-interval": "Интервал опроса", "label-series-limit": "Лимит рядов", "label-use-series-endpoint": "Использовать конечную точку ряда", - "more-info": "Более подробную информацию о настройке типа и версии Prometheus в источниках данных см. в <2>документации по подготовке.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Пример: {{example}}", "title-interval-behaviour": "Поведение интервалов", "title-other": "Прочее", "title-performance": "Производительность", "title-query-editor": "Редактор запросов", "tooltip-cache-level": "Устанавливает уровень кэширования в браузере для запросов редактора. Для источников данных с высокой кардинальностью рекомендуются более высокие настройки кэша.", - "tooltip-custom-query-parameters": "Добавьте пользовательские параметры в URL-адрес запроса Prometheus. Например, {{example1}}, {{example2}}, {{example3}}, или {{example4}}. Несколько параметров следует объединить с {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Установите параметр редактора по умолчанию для всех пользователей этого источника данных.", - "tooltip-disable-metrics-lookup": "При включении этого параметра будут отключены выбор метрик и поддержка метрик/меток при автозаполнении поля запроса. Функция полезна, если вы испытываете проблемы с производительностью при использовании более крупных экземляров Prometheus. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Функция отключит правила записи. Включите, чтобы улучшить производительность дашборда", "tooltip-http-method": "Для отправки запросов в источник данных Prometheus можно использовать HTTP-методы POST или GET. POST является рекомендуемым методом, поскольку позволяет выполнять более крупные запросы. Измените метод на GET, если вы используете версию Prometheus старше 2.1 или в вашей сети запрещены запросы POST.", "tooltip-incremental-querying-beta": "Функция изменит поведение относительных запросов по умолчанию. Вместо запроса свежих данных из экземпляра Prometheus, результаты запросов будут кэшироваться, и запрашиваться будут только новые записи. Включите, чтобы уменьшить нагрузку на базу данных и сеть.", @@ -479,7 +479,7 @@ "aria-label-selector": "селектор" }, "results-table": { - "content-descriptive-type": "При создании {{descriptiveType}} Prometheus отображает несколько рядов со счетчиком типов. ", + "content-descriptive-type": "", "description": "Описание", "message-expand-label-filters": "Метрики не найдены. Попробуйте расширить фильтры меток.", "message-expand-search": "Метрики не найдены. Попробуйте расширить поиск и фильтры.", diff --git a/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json index e41b22075dd..8132d631384 100644 --- a/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Den ursprungliga implementeringen av Prometheus variabla frågeredigerare. Ange en sträng med rätt frågetyp och parametrar enligt beskrivningen i dessa dokument. Till exempel, {{exampleQuery}}.", "tooltip-label": "Returnerar en lista över etikettvärden för etikettnamnet för alla mätvärden om inte mätvärdet är angivet.", "tooltip-metric-regex": "Returnerar en lista över etikettnamn, eventuellt filtrering efter angiven måttregex.", - "tooltip-query": "Returnerar en lista över Prometheus frågeresultat för frågan. Detta kan inkludera Prometheus-funktioner, d.v.s. {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Prometheus-datakällans tillägg tillhandahåller följande frågetyper för mallvariabler.", - "tooltip-series-query": "Ange ett mätvärde med etiketter, endast ett mätvärde eller endast etiketter d.v.s. {{example1}}, {{example2}} eller {{example3}}. Returnerar en lista över tidsserier som är associerade med angivna data." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "väljare", @@ -175,16 +175,16 @@ "label-scrape-interval": "Skrapningsintervall", "label-series-limit": "Seriegräns", "label-use-series-endpoint": "Använd serieändpunkt", - "more-info": "För mer information om hur du konfigurerar prometheustyp och -version i datakällor, se <2>provisioneringsdokumentationen.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Exempel: {{example}}", "title-interval-behaviour": "Intervallbeteende", "title-other": "Annat", "title-performance": "Performance", "title-query-editor": "Frågeredigerare", "tooltip-cache-level": "Anger webbläsarens cachelagringsnivå för redigeringsfrågor. Högre cacheinställningar rekommenderas för datakällor med hög kardinalitet.", - "tooltip-custom-query-parameters": "Lägg till anpassade parametrar i Prometheus fråge-URL. Till exempel {{example1}}, {{example2}}, {{example3}} eller {{example4}}. Flera parametrar ska sammanfogas med {{concatenationChar}}.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Ange standardalternativ för redigerare för alla användare av den här datakällan.", - "tooltip-disable-metrics-lookup": "Om du markerar det här alternativet inaktiveras metrikväljaren och stödet för automatisk komplettering av metrik/etikett i sökfältet. Detta hjälper om du har prestandaproblem med större Prometheus-instanser. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Denna funktion kommer att inaktivera inspelningsregler. Aktivera detta för att förbättra instrumentpanelens prestanda", "tooltip-http-method": "Du kan använda antingen HTTP-metoden POST eller GET för att fråga din Prometheus-datakälla. POST är den rekommenderade metoden eftersom den tillåter större frågor. Ändra detta till GET om du har en Prometheus-version som är äldre än 2.1 eller om POST-förfrågningar är begränsade i ditt nätverk.", "tooltip-incremental-querying-beta": "Denna funktion kommer att ändra standardbeteendet för relativa frågor till att alltid begära nya data från prometheus-instansen, istället kommer frågeresultat att cachelagras och endast nya poster begärs. Aktivera detta för att minska databas- och nätverksbelastningen.", @@ -479,7 +479,7 @@ "aria-label-selector": "väljare" }, "results-table": { - "content-descriptive-type": "När du skapar en {{descriptiveType}} visar Prometheus flera dataserier av typen räknare. ", + "content-descriptive-type": "", "description": "Beskrivning", "message-expand-label-filters": "Inga mätvärden hittades. Försök att utöka dina etikettfilter.", "message-expand-search": "Inga mätvärden hittades. Försök att utöka din sökning och dina filter.", diff --git a/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json index af4dea95932..4b918d3c61d 100644 --- a/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Prometheus değişken sorgu düzenleyicisinin orijinal uygulaması. Bu belgelerde açıklanan doğru sorgu türü ve parametrelerle bir dize girin. Örneğin {{exampleQuery}}.", "tooltip-label": "Bir metrik belirtilmedikçe tüm metriklerdeki etiket adı için etiket değerlerinin bir listesini döndürür.", "tooltip-metric-regex": "Belirtilen metrik düzenli ifadesine göre isteğe bağlı filtreleme yaparak etiket adlarının bir listesini döndürür.", - "tooltip-query": "Sorgu için Prometheus sorgu sonuçlarının bir listesini döndürür. Bu sorgular Prometheus işlevlerini içerebilir, örneğin {{exampleQuery}}.", + "tooltip-query": "", "tooltip-query-type": "Prometheus veri kaynağı eklentisi, şablon değişkenleri için aşağıdaki sorgu türlerini sağlar.", - "tooltip-series-query": "Bir metrik ve etiketlerle, yalnızca metrik ya da yalnızca etiketlerle birlikte bir değer girin. Örneğin {{example1}}, {{example2}} veya {{example3}}. Girilen verilerle ilişkili zaman serilerinin bir listesini döndürür." + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "seçici", @@ -175,16 +175,16 @@ "label-scrape-interval": "Kazıma aralığı", "label-series-limit": "Seri sınırı", "label-use-series-endpoint": "Seri uç noktasını kullan", - "more-info": "Prometheus türü ve sürümünü veri kaynaklarında yapılandırma hakkında daha fazla bilgi için <2>sağlama belgelerine bakın.", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "Örnek: {{example}}", "title-interval-behaviour": "Aralık davranışı", "title-other": "Diğer", "title-performance": "Performans", "title-query-editor": "Sorgu düzenleyicisi", "tooltip-cache-level": "Düzenleyici sorguları için tarayıcı önbelleğe alma düzeyini ayarlar. Yüksek ayırt ediciliğe sahip veri kaynakları için daha yüksek önbellek ayarları önerilir.", - "tooltip-custom-query-parameters": "Prometheus sorgu URL'sine özel parametreler ekleyin. Örneğin {{example1}}, {{example2}}, {{example3}} veya {{example4}}. Birden fazla parametre, {{concatenationChar}} karakteri ile birleştirilmelidir.", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "Bu veri kaynağının tüm kullanıcıları için varsayılan düzenleyici seçeneğini ayarlayın.", - "tooltip-disable-metrics-lookup": "Bu seçeneği işaretlemek, metrik seçicisini ve sorgu alanındaki metrik/etiket desteğini otomatik tamamlama özelliğinde devre dışı bırakır. Bu, büyük Prometheus örneklerinde yaşanan performans sorunlarına yardımcı olabilir. ", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "Bu özellik, kayıt kurallarını devre dışı bırakacaktır. Pano performansını artırmak için bu seçeneği etkinleştirin", "tooltip-http-method": "Prometheus veri kaynağınızı sorgulamak için POST veya GET HTTP yöntemini kullanabilirsiniz. POST, daha büyük sorgulara izin verdiği için önerilen yöntemdir. Prometheus sürümünüz 2.1'den eskiyse ya da ağınızda POST istekleri kısıtlıysa bunu GET olarak değiştirin.", "tooltip-incremental-querying-beta": "Bu özellik, göreli sorguların varsayılan davranışını değiştirerek Prometheus örneğinden her zaman güncel veri istenmesini sağlar. Bunun yerine, sorgu sonuçları önbelleğe alınır ve yalnızca yeni kayıtlar istenir. Veri tabanı ve ağ yükünü azaltmak için bu özelliği etkinleştirin.", @@ -479,7 +479,7 @@ "aria-label-selector": "seçici" }, "results-table": { - "content-descriptive-type": "Bir {{descriptiveType}} oluşturulduğunda, Prometheus sayaç türünde birden fazla seri sunar. ", + "content-descriptive-type": "", "description": "Açıklama", "message-expand-label-filters": "Metrik bulunamadı. Etiket filtrelerinizi genişletmeyi deneyin.", "message-expand-search": "Metrik bulunamadı. Aramanızı ve filtrelerinizi genişletmeyi deneyin.", diff --git a/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json index 890add6aa2f..b8f110e9226 100644 --- a/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Prometheus 变量查询编辑器的原始执行操作。按照这些文档中所述,输入包含正确查询类型和参数的字符串。例如,{{exampleQuery}}。", "tooltip-label": "返回所有指标中标签名称的标签值列表,除非指定了指标。", "tooltip-metric-regex": "返回标签名称列表,可选择按指定的指标正则表达式进行筛选。", - "tooltip-query": "返回查询的 Prometheus 查询结果列表。这可以包含 Prometheus 函数,即 {{exampleQuery}}。", + "tooltip-query": "", "tooltip-query-type": "Prometheus 数据源插件为模板变量提供以下查询类型。", - "tooltip-series-query": "输入带有标签的指标、仅指标或仅标签,即 {{example1}}、{{example2}} 或 {{example3}}。返回与输入数据关联的时间序列列表。" + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "选择器", @@ -175,16 +175,16 @@ "label-scrape-interval": "抓取间隔", "label-series-limit": "序列限制", "label-use-series-endpoint": "使用序列端点", - "more-info": "有关在数据源中配置 prometheus 类型和版本的更多信息,请参阅<2>预配文档。", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "示例:{{example}}", "title-interval-behaviour": "间隔行为", "title-other": "其他", "title-performance": "性能", "title-query-editor": "查询编辑器", "tooltip-cache-level": "设置编辑器查询的浏览器缓存级别。对于高基数数据源,建议使用更高的缓存设置。", - "tooltip-custom-query-parameters": "将自定义参数添加到 Prometheus 查询网址。例如 {{example1}}、{{example2}}、{{example3}} 或 {{example4}}。多个参数应使用 {{concatenationChar}} 连接起来。", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "为此数据源的所有用户设置默认编辑器选项。", - "tooltip-disable-metrics-lookup": "选中此选项将禁用查询字段自动完成中的指标选择器和指标/标签支持。如果您在使用较大的 Prometheus 实例时遇到性能问题,这样做会有所帮助。", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "此功能将禁用录制规则。开启此选项可提高数据面板性能", "tooltip-http-method": "您可以使用 POST 或 GET HTTP 方法来查询 Prometheus 数据源。POST 是推荐的方法,因为它允许更大的查询。如果您的 Prometheus 版本低于 2.1,或者您的网络中限制了 POST 请求,请将其更改为 GET。", "tooltip-incremental-querying-beta": "此功能将改变相对查询的默认行为,使其总是从 Prometheus 实例请求新数据,而不会缓存查询结果,且只请求新数据。开启此选项可减少数据库和网络负载。", @@ -479,7 +479,7 @@ "aria-label-selector": "选择器" }, "results-table": { - "content-descriptive-type": "创建 {{descriptiveType}} 时,Prometheus 会使用类型计数器公开多个序列。", + "content-descriptive-type": "", "description": "描述", "message-expand-label-filters": "未找到指标。尝试展开您的标签筛选条件。", "message-expand-search": "未找到指标。尝试展开您的搜索和筛选条件。", diff --git a/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json index 0f4fd89eaa2..c9b5b8bdd99 100644 --- a/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json @@ -85,9 +85,9 @@ "tooltip-classic-query": "Prometheus 變數查詢編輯器的原始實作。輸入一個字串,其中包含如這些文件中所述正確的查詢類型和參數。例如:{{exampleQuery}}。", "tooltip-label": "除非有指定指標,否則會回傳所有指標中該標籤名稱的標籤值清單。", "tooltip-metric-regex": "回傳標籤名稱清單,選擇性地依據指定指標正規表達式篩選。", - "tooltip-query": "回傳此查詢的 Prometheus 查詢結果清單。這可能包括 Prometheus 功能,即{{exampleQuery}}。", + "tooltip-query": "", "tooltip-query-type": "Prometheus 資料來源外掛程式為範本變數提供以下查詢類型。", - "tooltip-series-query": "輸入帶有標籤的指標、僅有指標或僅標籤,即 {{example1}}、{{example2}} 或 {{example3}}。回傳與輸入資料相關的時間序列清單。" + "tooltip-series-query": "" }, "selector-actions": { "aria-label-selector": "選擇器", @@ -175,16 +175,16 @@ "label-scrape-interval": "抓取間隔", "label-series-limit": "序列限制", "label-use-series-endpoint": "使用序列端點", - "more-info": "如需更多在資料來源中設定 Prometheus 類型與版本的相關資訊,請參閱<2>佈建文件。", + "more-info": "", "placeholder-example-maxsourceresolutionmtimeout": "範例:{{example}}", "title-interval-behaviour": "間隔行為", "title-other": "其他", "title-performance": "成效", "title-query-editor": "查詢編輯器", "tooltip-cache-level": "設定編輯器查詢的瀏覽器快取層級。針對高基數資料來源,建議使用較高層級的快取設定。", - "tooltip-custom-query-parameters": "將自訂參數新增至 Prometheus 查詢網址。例如 {{example1}}、{{example2}}、{{example3}} 或 {{example4}}。多個參數應與{{concatenationChar}}串接在一起。", + "tooltip-custom-query-parameters": "", "tooltip-default-editor": "為此資料來源的所有使用者設定預設編輯器選項。", - "tooltip-disable-metrics-lookup": "勾選此選項將停用查詢欄位自動完成中的指標選擇器和指標/標籤支援。如果您在較大的 Prometheus 實例上遇到效能問題,這會有所幫助。", + "tooltip-disable-metrics-lookup": "", "tooltip-disable-recording-rules-beta": "此功能將停用錄製規則。開啟此功能以改善儀表板效能", "tooltip-http-method": "您可以使用 POST 或 GET HTTP 方法,來查詢您的 Prometheus 資料來源。POST 是建議的方法,因為它可以處理更大規模的查詢。如果您的 Prometheus 版本早於 2.1,或您的網路中限制了 POST 請求,請將此更改為 GET。", "tooltip-incremental-querying-beta": "此功能將變更相對時間查詢的預設行為,以便隨時從 Prometheus 實例要求新資料,而不會快取查詢結果,並且只會請求新紀錄。開啟此功能以減少資料庫和網路負載。", @@ -479,7 +479,7 @@ "aria-label-selector": "選擇器" }, "results-table": { - "content-descriptive-type": "建立{{descriptiveType}}時,Prometheus 會透過類型計數器顯示多個序列。", + "content-descriptive-type": "", "description": "說明", "message-expand-label-filters": "未找到指標。請嘗試展開標籤篩選條件。", "message-expand-search": "未找到指標。請嘗試擴大搜尋範圍和篩選條件。", diff --git a/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx b/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx index a4f17087b21..2364bc80a86 100644 --- a/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx +++ b/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx @@ -111,7 +111,6 @@ export const QueryPatternsModal = (props: Props) => { } )} isOpen={isOpen} - collapsible={true} onToggle={() => { const action = isOpen ? 'close' : 'open'; reportInteraction(`grafana_prom_kickstart_toggle_pattern_card`, { diff --git a/packages/grafana-prometheus/src/querybuilder/shared/QueryOptionGroup.tsx b/packages/grafana-prometheus/src/querybuilder/shared/QueryOptionGroup.tsx index 554201a820d..8253d195ea0 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/QueryOptionGroup.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/QueryOptionGroup.tsx @@ -20,7 +20,6 @@ export function QueryOptionGroup({ title, children, collapsedInfo }: Props) {
= { up: 'green', }; -export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip { +export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithAnnotations { /** * Sets the style of the candlesticks */ diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 13939c556d6..fea3266ca6d 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export enum HorizontalConstraint { Center = 'center', diff --git a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts index 2334d1d1efd..eb0dd667076 100644 --- a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface MetricStat { /** @@ -218,6 +218,11 @@ export interface QueryEditorArrayExpression { export type QueryEditorExpression = (QueryEditorArrayExpression | QueryEditorPropertyExpression | QueryEditorGroupByExpression | QueryEditorFunctionExpression | QueryEditorFunctionParameterExpression | QueryEditorOperatorExpression); +export enum LogsMode { + Anomalies = 'Anomalies', + Insights = 'Insights', +} + export enum LogsQueryLanguage { CWLI = 'CWLI', PPL = 'PPL', @@ -241,6 +246,10 @@ export interface CloudWatchLogsQuery extends common.DataQuery { * Log groups to query */ logGroups?: Array; + /** + * Whether a query is a Logs Insights or Log Anomalies query + */ + logsMode?: LogsMode; /** * Language used for querying logs, can be CWLI, SQL, or PPL. If empty, the default language is CWLI. */ @@ -265,6 +274,33 @@ export const defaultCloudWatchLogsQuery: Partial = { statsGroups: [], }; +/** + * Shape of a Cloudwatch Log Anomalies query + */ +export interface CloudWatchLogsAnomaliesQuery extends common.DataQuery { + /** + * Used to filter only the anomalies found by a certain anomaly detector + */ + anomalyDetectionARN?: string; + id: string; + /** + * Whether a query is a Logs Insights or Log Anomalies query + */ + logsMode?: LogsMode; + /** + * Whether a query is a Metrics, Logs or Annotations query + */ + queryMode?: CloudWatchQueryMode; + /** + * AWS region to query for the logs + */ + region: string; + /** + * Filter to return only anomalies that are 'SUPPRESSED', 'UNSUPPRESSED', or 'ALL' (default) + */ + suppressionState?: string; +} + export interface LogGroup { /** * AccountId of the log group diff --git a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts index a1ec65bf148..5b65d042e72 100644 --- a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts index 65ef7e9883d..cfc4efd5f22 100644 --- a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options { selectedSeries: number; diff --git a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts index 443add70b78..d71d823d38f 100644 --- a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export type UpdateConfig = { render: boolean, diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index d63d368d534..ad46e229611 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested); diff --git a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts index ac1fd6808c2..f68950c8cb3 100644 --- a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options extends common.SingleStatBaseOptions { minVizHeight: number; diff --git a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts index 4819a4cfc1b..74bdafa169d 100644 --- a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options { basemap: ui.MapLayerOptions; diff --git a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts index d905b1bd2ea..1ff4370c9a2 100644 --- a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; /** * Controls the color mode of the heatmap @@ -188,6 +188,7 @@ export interface RowsHeatmapOptions { } export interface Options { + annotations?: ui.VizAnnotations; /** * Controls if the heatmap should be calculated from data */ diff --git a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts index 2d5705e31e5..aa104192e1a 100644 --- a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip { /** diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index 9ea5e96bad3..f8e672a8979 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options { controlsStorageKey?: string; diff --git a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts index 045b217482f..5f604cebfae 100644 --- a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface GaugePanelEffects { barGlow?: boolean; diff --git a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts index 5f46545f820..e26eabbdee2 100644 --- a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts index 17fe57fcc44..0c980fc8495 100644 --- a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface ArcOption { /** diff --git a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts index 067c6d7d146..8a3f0bbe55c 100644 --- a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; /** * Select the pie chart display style. diff --git a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts index 0fb48020d23..f12b46fd8f0 100644 --- a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options extends common.SingleStatBaseOptions { colorMode: common.BigValueColorMode; diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index b8220d24d3a..1c327e1b8c0 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -10,9 +10,9 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; -export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { +export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones, ui.OptionsWithAnnotations { /** * Controls value alignment on the timelines */ diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index 030a310ff8b..48825cb2b58 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -10,9 +10,9 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; -export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { +export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones, ui.OptionsWithAnnotations { /** * Controls the column width */ diff --git a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts index 3964c70f630..c45151fed2b 100644 --- a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts index 39053ebb9af..c4695995725 100644 --- a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export enum TextMode { Code = 'code', diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index 538860716da..d0213051004 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -10,9 +10,9 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; -export interface Options extends common.OptionsWithTimezones { +export interface Options extends common.OptionsWithTimezones, common.OptionsWithAnnotations { legend: common.VizLegendOptions; orientation?: common.VizOrientation; timeCompare?: common.TimeCompareOptions; diff --git a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts index 7ec6ddd5686..fc0bcd92a4d 100644 --- a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; /** * Identical to timeseries... except it does not have timezone settings diff --git a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts index 450721304fe..5e851caa415 100644 --- a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.3.0-pre"; +export const pluginVersion = "12.4.0-pre"; export enum PointShape { Circle = 'circle', diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index b370fc6206c..3466df584b8 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -106,6 +106,10 @@ export interface AnnotationQuery { * Name of annotation. */ name: string; + /** + * Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + */ + placement?: AnnotationQueryPlacement; /** * TODO.. this should just be a normal query target */ @@ -363,6 +367,12 @@ export type DashboardLinkType = ('link' | 'dashboards'); */ export type DashboardLinkPlacement = 'inControlsMenu'; +/** + * Annotation Query placement. Defines where the annotation query should be displayed. + * - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu + */ +export type AnnotationQueryPlacement = 'inControlsMenu'; + /** * Dashboard action type */ @@ -885,6 +895,11 @@ export interface Panel { * Depends on the panel plugin. See the plugin documentation for details. */ targets?: Array>; + /** + * Compare the current time range with a previous period + * For example "1d" to compare current period but shifted back 1 day + */ + timeCompare?: string; /** * Overrides the relative time range for individual panels, * which causes them to be different than what is selected in diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 5369503d6f2..ba637656ed8 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -409,6 +409,7 @@ QueryOptionsSpec: { interval?: string cacheTimeout?: string hideTimeOverride?: bool + timeCompare?: string } DataQueryKind: { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 6ecb17f191b..3cb2c8aab5b 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -259,6 +259,8 @@ export interface FieldConfigSource { defaults: FieldConfig; // Overrides are the options applied to specific fields overriding the defaults. overrides: { + // Describes config override rules created when interacting with Grafana. + __systemRef?: string; matcher: MatcherConfig; properties: DynamicConfigValue[]; }[]; @@ -457,7 +459,8 @@ export type ThresholdsMode = "absolute" | "percentage"; export const defaultThresholdsMode = (): ThresholdsMode => ("absolute"); export interface Threshold { - value: number; + // Value null means -Infinity + value: number | null; color: string; } diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index 19c324f1cd9..00e9111453a 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -18,6 +18,8 @@ export interface AnnotationQuerySpec { name: string; builtIn?: boolean; filter?: AnnotationPanelFilter; + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: "inControlsMenu"; // Catch-all field for datasource-specific properties. Should not be available in as code tooling. legacyOptions?: Record; } @@ -29,6 +31,7 @@ export const defaultAnnotationQuerySpec = (): AnnotationQuerySpec => ({ iconColor: "", name: "", builtIn: false, + placement: AnnotationQueryPlacement, }); export interface DataQueryKind { @@ -62,6 +65,10 @@ export const defaultAnnotationPanelFilter = (): AnnotationPanelFilter => ({ ids: [], }); +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +export const AnnotationQueryPlacement = "inControlsMenu"; + // "Off" for no shared crosshair or tooltip (default). // "Crosshair" for shared crosshair. // "Tooltip" for shared crosshair AND shared tooltip. @@ -155,7 +162,7 @@ export interface PanelQuerySpec { export const defaultPanelQuerySpec = (): PanelQuerySpec => ({ query: defaultDataQueryKind(), - refId: "", + refId: "A", hidden: false, }); @@ -219,6 +226,7 @@ export interface QueryOptionsSpec { interval?: string; cacheTimeout?: string; hideTimeOverride?: boolean; + timeCompare?: string; } export const defaultQueryOptionsSpec = (): QueryOptionsSpec => ({ @@ -258,6 +266,8 @@ export interface FieldConfigSource { defaults: FieldConfig; // Overrides are the options applied to specific fields overriding the defaults. overrides: { + // Describes config override rules created when interacting with Grafana. + __systemRef?: string; matcher: MatcherConfig; properties: DynamicConfigValue[]; }[]; @@ -456,7 +466,8 @@ export type ThresholdsMode = "absolute" | "percentage"; export const defaultThresholdsMode = (): ThresholdsMode => ("absolute"); export interface Threshold { - value: number; + // Value null means -Infinity + value: number | null; color: string; } @@ -1274,7 +1285,7 @@ export interface IntervalVariableSpec { auto: boolean; auto_min: string; auto_count: number; - refresh: VariableRefresh; + refresh: "onTimeRangeChanged"; label?: string; hide: VariableHide; skipUrlSync: boolean; @@ -1289,7 +1300,7 @@ export const defaultIntervalVariableSpec = (): IntervalVariableSpec => ({ auto: false, auto_min: "", auto_count: 0, - refresh: "never", + refresh: "onTimeRangeChanged", hide: "dontHide", skipUrlSync: false, }); diff --git a/packages/grafana-sql/i18next.config.ts b/packages/grafana-sql/i18next.config.ts new file mode 100644 index 00000000000..30e4acf0e63 --- /dev/null +++ b/packages/grafana-sql/i18next.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'i18next-cli'; + +export default defineConfig({ + locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages + extract: { + input: ['src/**/*.{tsx,ts}'], + output: 'src/locales/{{language}}/{{namespace}}.json', + defaultNS: 'grafana-sql', + functions: ['t', '*.t'], + transComponents: ['Trans'], + }, +}); diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 4d97947a200..321cfccfe7b 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "@grafana/sql", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git", @@ -12,16 +12,16 @@ "main": "src/index.ts", "scripts": { "typecheck": "tsc --emitDeclarationOnly false --noEmit", - "i18n-extract": "i18next --config src/locales/i18next-parser.config.cjs" + "i18n-extract": "i18next-cli extract --sync-primary" }, "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.3.0-pre", - "@grafana/e2e-selectors": "12.3.0-pre", - "@grafana/i18n": "12.3.0-pre", + "@grafana/data": "12.4.0-pre", + "@grafana/e2e-selectors": "12.4.0-pre", + "@grafana/i18n": "12.4.0-pre", "@grafana/plugin-ui": "^0.10.10", - "@grafana/runtime": "12.3.0-pre", - "@grafana/ui": "12.3.0-pre", + "@grafana/runtime": "12.4.0-pre", + "@grafana/ui": "12.4.0-pre", "@react-awesome-query-builder/ui": "6.6.15", "immutable": "5.1.4", "lodash": "4.17.21", @@ -42,13 +42,13 @@ "@testing-library/user-event": "14.6.1", "@types/jest": "^29.5.4", "@types/lodash": "4.17.20", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-virtualized-auto-sizer": "1.0.8", "@types/systemjs": "6.15.3", "@types/uuid": "10.0.0", - "i18next-parser": "9.3.0", + "i18next-cli": "1.11.12", "jest": "^29.6.4", "ts-jest": "29.4.0", "ts-node": "10.9.2", diff --git a/packages/grafana-sql/src/components/ErrorBoundary.tsx b/packages/grafana-sql/src/components/ErrorBoundary.tsx index f7991415795..8500fb68fdf 100644 --- a/packages/grafana-sql/src/components/ErrorBoundary.tsx +++ b/packages/grafana-sql/src/components/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import { Component, ErrorInfo, PropsWithChildren } from 'react'; import { Trans } from '@grafana/i18n'; @@ -6,14 +6,14 @@ type Props = { fallBackComponent?: React.ReactNode; }; -export class ErrorBoundary extends React.Component, { hasError: boolean }> { - constructor(props: React.PropsWithChildren) { +export class ErrorBoundary extends Component, { hasError: boolean }> { + constructor(props: PropsWithChildren) { super(props); this.state = { hasError: false }; } - static getDerivedStateFromError() { - return { hasError: true }; + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.setState({ hasError: true }); } render() { diff --git a/packages/grafana-sql/src/locales/cs-CZ/grafana-sql.json b/packages/grafana-sql/src/locales/cs-CZ/grafana-sql.json index 68f1dd03de9..d908975dc53 100644 --- a/packages/grafana-sql/src/locales/cs-CZ/grafana-sql.json +++ b/packages/grafana-sql/src/locales/cs-CZ/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Auto. max. nečinný", - "content-auto-max-idle": "Pokud je tato možnost povolena, automaticky nastaví počet <1>Maximálních nečinných připojení na stejnou hodnotu jako<3> Maximální otevřená připojení. Pokud není nastaven počet maximálních otevřených připojení, bude nastaven na výchozí hodnotu ({{defaultMaxIdle}}).", - "content-max-idle": "Maximální počet připojení v nečinném připojovacím souboru. Pokud je <1>Maximální počet otevřených připojení větší než 0, ale menší než <3>Maximální počet nečinných připojení, pak bude <5>Maximální počet nečinných připojení snížen tak, aby odpovídal limitu pro <8>Maximální počet otevřených připojení. Pokud je nastaveno na 0, nejsou zachována žádná nečinná připojení.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "Maximální doba v sekundách, po kterou může být připojení znovu použito. Pokud je nastaveno na 0, připojení jsou znovu používána opakovaně.", - "content-max-open": "Maximální počet otevřených připojení k databázi. Pokud je <1>Maximální počet nečinných připojení větší než 0 a <3>Maximální počet otevřených připojení je menší než <5>Maximální počet nečinných připojení, pak bude <7>Maximální počet nečinných připojení snížen tak, aby odpovídal limitu pro <9>Maximální počet otevřených připojení. Pokud je nastaveno na 0, počet otevřených připojení není omezen.", + "content-max-open": "", "max-idle": "Max. nečinný", "max-lifetime": "Max. životnost", "max-open": "Max. otevřený", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Váš dotaz je neplatný. Podrobnosti najdete níže. <1>Tento dotaz však můžete stále spustit.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Nástroj pro tvorbu", "label-code": "Kód" @@ -76,7 +76,7 @@ "tooltip-format-query": "Formátovat dotaz" }, "query-validator": { - "query-will-process": "<0> Tento dotaz zpracuje <2>{{bytes}} při spuštění.", + "query-will-process": "", "validating-query": "Ověřování dotazu…" }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/de-DE/grafana-sql.json b/packages/grafana-sql/src/locales/de-DE/grafana-sql.json index 784acba470c..a8298401968 100644 --- a/packages/grafana-sql/src/locales/de-DE/grafana-sql.json +++ b/packages/grafana-sql/src/locales/de-DE/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Auto. max. inaktiv", - "content-auto-max-idle": "Wenn dies aktiviert ist, wird die Anzahl der <1>Maximalen inaktiven Verbindungen automatisch auf den gleichen Wert der<3> Maximalen offenen Verbindungen festgelegt. Wenn die Anzahl der maximalen offenen Verbindungen nicht festgelegt ist, wird sie auf den Standardwert ({{defaultMaxIdle}}) eingestellt.", - "content-max-idle": "Die maximale Anzahl der Verbindungen im Pool der inaktiven Verbindungen. Wenn <1>Maximale offene Verbindungen größer ist als 0, aber kleiner als die Anzahl der <3>Maximalen inaktiven Verbindungen, werden die <5>Maximalen inaktiven Verbindungen verringert, um der Grenze für die Anzahl der <8>Maximalen offenen Verbindungen zu entsprechen. Wenn der Wert auf 0 eingestellt ist, werden keine inaktiven Verbindungen beibehalten.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "Die maximale Zeitdauer der Wiederverwendung einer Verbindung in Sekunden. Bei einer Einstellung auf 0 werden Verbindungen immer wiederverwendet.", - "content-max-open": "Die maximale Anzahl offener Verbindungen zur Datenbank. Wenn <1>Maximale inaktive Verbindungen größer als 0 ist und die Anzahl der <3>Maximalen offenen Verbindungen kleiner als die Anzahl der <5>Maximalen inaktiven Verbindungen ist, werden die <7>Maximalen inaktiven Verbindungen bis zur Grenze für <9>Maximale offene Verbindungen verringert. Wenn 0 festgelegt ist, gibt es keine Begrenzung der Anzahl offener Verbindungen.", + "content-max-open": "", "max-idle": "Max. inaktiv", "max-lifetime": "Max. Lebensdauer", "max-open": "Max. offen", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Ihre Abfrage ist ungültig. Weitere Informationen finden Sie unten. <1>Sie können diese Abfrage aber trotzdem ausführen.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Builder", "label-code": "Code" @@ -76,7 +76,7 @@ "tooltip-format-query": "Formatabfrage" }, "query-validator": { - "query-will-process": "<0> Diese Abfrage verarbeitet <2>{{bytes}}, wenn sie ausgeführt wird.", + "query-will-process": "", "validating-query": "Abfrage wird validiert …" }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/en-US/grafana-sql.json b/packages/grafana-sql/src/locales/en-US/grafana-sql.json index bc986568f18..f2a06d8b983 100644 --- a/packages/grafana-sql/src/locales/en-US/grafana-sql.json +++ b/packages/grafana-sql/src/locales/en-US/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Auto max idle", - "content-auto-max-idle": "If enabled, automatically set the number of <1>Maximum idle connections to the same value as<3> Max open connections. If the number of maximum open connections is not set it will be set to the default ({{defaultMaxIdle}}).", - "content-max-idle": "The maximum number of connections in the idle connection pool.If <1>Max open connections is greater than 0 but less than the <3>Max idle connections, then the <5>Max idle connections will be reduced to match the <8>Max open connections limit. If set to 0, no idle connections are retained.", + "content-auto-max-idle": "If enabled, automatically set the number of Maximum idle connections to the same value as Max open connections. If the number of maximum open connections is not set it will be set to the default ({{defaultMaxIdle}}).", + "content-max-idle": "The maximum number of connections in the idle connection pool.If Max open connections is greater than 0 but less than the Max idle connections, then the Max idle connections will be reduced to match the Max open connections limit. If set to 0, no idle connections are retained.", "content-max-lifetime": "The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.", - "content-max-open": "The maximum number of open connections to the database. If <1>Max idle connections is greater than 0 and the <3>Max open connections is less than <5>Max idle connections, then<7>Max idle connections will be reduced to match the <9>Max open connections limit. If set to 0, there is no limit on the number of open connections.", + "content-max-open": "The maximum number of open connections to the database. If Max idle connections is greater than 0 and the Max open connections is less than Max idle connections, then Max idle connections will be reduced to match the Max open connections limit. If set to 0, there is no limit on the number of open connections.", "max-idle": "Max idle", "max-lifetime": "Max lifetime", "max-open": "Max open", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Your query is invalid. Check below for details. <1>However, you can still run this query.", + "content-invalid-query": "Your query is invalid. Check below for details.
However, you can still run this query.", "editor-modes": { "label-builder": "Builder", "label-code": "Code" @@ -76,7 +76,7 @@ "tooltip-format-query": "Format query" }, "query-validator": { - "query-will-process": "<0> This query will process <2>{{bytes}} when run.", + "query-will-process": "<0> This query will process {{bytes}} when run.", "validating-query": "Validating query..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/es-ES/grafana-sql.json b/packages/grafana-sql/src/locales/es-ES/grafana-sql.json index 50762539f05..9937632d196 100644 --- a/packages/grafana-sql/src/locales/es-ES/grafana-sql.json +++ b/packages/grafana-sql/src/locales/es-ES/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Inactividad máxima automática", - "content-auto-max-idle": "Si está habilitado, establece automáticamente el número de <1>Conexiones inactivas máximas en el mismo valor que<3> Conexiones abiertas máximas. Si no se establece el número de conexiones abiertas máximas, se establecerá en el valor predeterminado ({{defaultMaxIdle}}).", - "content-max-idle": "El número máximo de conexiones en el grupo de conexiones inactivas. Si <1>Conexiones abiertas máximas es mayor que 0 pero menor que <3>Conexiones inactivas máximas, entonces <5>Conexiones inactivas máximas se reducirá para que coincida con el límite de <8>Conexiones abiertas máximas. Si se establece en 0, no se conservan las conexiones inactivas.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "La cantidad máxima de tiempo en segundos que se puede reutilizar una conexión. Si se establece en 0, las conexiones se reutilizan para siempre.", - "content-max-open": "El número máximo de conexiones abiertas a la base de datos. Si <1>Conexiones inactivas máximas es mayor que 0 y <3>Conexiones abiertas máximas es menor que <5>Conexiones inactivas máximas, entonces <7>Conexiones inactivas máximas se reducirá para que coincida con el límite de <9>Conexiones abiertas máximas. Si se establece en 0, no hay límite en el número de conexiones abiertas.", + "content-max-open": "", "max-idle": "Máx. inactivas", "max-lifetime": "Vida útil máxima", "max-open": "Máx. abiertas", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Su consulta no es válida. A continuación podrá obtener más información. <1>En todo caso, puede ejecutar esta consulta.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Constructor", "label-code": "Código" @@ -76,7 +76,7 @@ "tooltip-format-query": "Formatear consulta" }, "query-validator": { - "query-will-process": "<0> Esta consulta procesará <2>{{bytes}} cuando se ejecute.", + "query-will-process": "", "validating-query": "Validando consulta..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/fr-FR/grafana-sql.json b/packages/grafana-sql/src/locales/fr-FR/grafana-sql.json index f125322db6f..e24bc1fc4ec 100644 --- a/packages/grafana-sql/src/locales/fr-FR/grafana-sql.json +++ b/packages/grafana-sql/src/locales/fr-FR/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Inactivité maximale automatique", - "content-auto-max-idle": "Si cette option est activée, le nombre de <1>connexions inactives maximales est automatiquement défini sur la même valeur que<3> Connexions ouvertes maximales. Si le nombre maximal de connexions ouvertes n’est pas défini, il sera défini sur la valeur par défaut ({{defaultMaxIdle}}).", - "content-max-idle": "Le nombre maximal de connexions dans le pool de connexions inactives. Si <1>Connexions ouvertes maximales est supérieur à 0, mais inférieur à <3>Connexions inactives maximales, alors <5>Connexions inactives maximales sera réduit pour correspondre à la limite de <8>Connexions ouvertes maximales. Si la valeur est 0, aucune connexion inactive n’est conservée.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "La durée maximale en secondes pendant laquelle une connexion peut être réutilisée. Si la valeur est 0, les connexions sont réutilisées pour toujours.", - "content-max-open": "Le nombre maximal de connexions ouvertes à la base de données. Si <1>Connexions inactives maximales est supérieur à 0 et que <3>Connexions ouvertes maximales est inférieur à <5>Connexions inactives maximales, alors <7>Connexions inactives maximales sera réduit pour correspondre à la limite de <9>Connexions ouvertes maximales. Si la valeur est 0, il n’y a pas de limite quant au nombre de connexions ouvertes.", + "content-max-open": "", "max-idle": "Inactivité maximale", "max-lifetime": "Durée de vie maximale", "max-open": "Ouverture maximale", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Votre requête n’est pas valide. Vérifiez ci-dessous pour en savoir plus. <1>Cependant, vous pouvez toujours exécuter cette requête.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Builder", "label-code": "Code" @@ -76,7 +76,7 @@ "tooltip-format-query": "Requête de format" }, "query-validator": { - "query-will-process": "<0> Cette requête traitera <2>{{bytes}} lors de son exécution.", + "query-will-process": "", "validating-query": "Validation de la requête..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/hu-HU/grafana-sql.json b/packages/grafana-sql/src/locales/hu-HU/grafana-sql.json index 2f48efc048e..a5f7f0b2996 100644 --- a/packages/grafana-sql/src/locales/hu-HU/grafana-sql.json +++ b/packages/grafana-sql/src/locales/hu-HU/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Inaktív automatikus maximális száma", - "content-auto-max-idle": "Ha engedélyezve van, automatikusan beállítja az <1>Inaktív kapcsolatok maximális száma értéket a <3>Megnyitott kapcsolatok maximális száma értékére. Ha a nyitott kapcsolatok maximális száma nincs beállítva, akkor az alapértelmezett értékre lesz beállítva ({{defaultMaxIdle}}).", - "content-max-idle": "Az inaktív kapcsolatok készletében lévő kapcsolatok maximális száma. Ha a <1>Megnyitott kapcsolatok maximális száma nagyobb, mint 0, de kisebb, mint az <3>Inaktív kapcsolatok maximális száma, akkor az <5>Inaktív kapcsolatok maximális száma csökken, hogy megfeleljen a <8>Megnyitott kapcsolatok maximális száma határértékének. Ha 0-ra van állítva, akkor egyetlen inaktív kapcsolat sem marad meg.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "A csatlakozás újrafelhasználásának maximális időtartama másodpercben. Ha 0-ra van állítva, a kapcsolatok újrafelhasználása végtelen.", - "content-max-open": "Az adatbázishoz irányuló nyitott kapcsolatok maximális száma. Ha az <1>Inaktív kapcsolatok maximális száma nagyobb, mint 0, és a <3>Megnyitott kapcsolatok maximális száma kisebb, mint az <5>Inaktív kapcsolatok maximális száma, akkor az <7>Inaktív kapcsolatok maximális száma csökken, hogy megfeleljen a <9>Megnyitott kapcsolatok maximális száma határértékének. Ha 0-ra van állítva, akkor nincs korlátozva a nyitott kapcsolatok száma.", + "content-max-open": "", "max-idle": "Max. inaktív", "max-lifetime": "Max. élettartam", "max-open": "Max. nyitott", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "A lekérdezés érvénytelen. A részleteket tekintse meg alább. <1>Ezt a lekérdezést azonban továbbra is futtathatja.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Építő", "label-code": "Kód" @@ -76,7 +76,7 @@ "tooltip-format-query": "Lekérdezés formázása" }, "query-validator": { - "query-will-process": "<0> Ez a lekérdezés <2>{{bytes}} feldolgozását végzi futtatáskor.", + "query-will-process": "", "validating-query": "Lekérdezés érvényesítése…" }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/i18next-parser.config.cjs b/packages/grafana-sql/src/locales/i18next-parser.config.cjs deleted file mode 100644 index 9ab1f779724..00000000000 --- a/packages/grafana-sql/src/locales/i18next-parser.config.cjs +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - locales: ['en-US'], // Only en-US is updated - Crowdin will PR with other languages - sort: true, - createOldCatalogs: false, - failOnWarnings: true, - verbose: false, - resetDefaultValueLocale: 'en-US', // Updates extracted values when they change in code - - defaultNamespace: 'grafana-sql', - input: ['../**/*.{tsx,ts}'], - output: './src/locales/$LOCALE/$NAMESPACE.json', -}; diff --git a/packages/grafana-sql/src/locales/id-ID/grafana-sql.json b/packages/grafana-sql/src/locales/id-ID/grafana-sql.json index 7d4256c43eb..e0233f65172 100644 --- a/packages/grafana-sql/src/locales/id-ID/grafana-sql.json +++ b/packages/grafana-sql/src/locales/id-ID/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Nonaktif maksimum otomatis", - "content-auto-max-idle": "Jika diaktifkan, secara otomatis atur jumlah <1>Koneksi nonaktif maksimum ke nilai yang sama dengan<3> Koneksi terbuka maksimum. Jika jumlah maksimum koneksi terbuka tidak diatur, jumlah tersebut akan diatur ke default ({{defaultMaxIdle}}).", - "content-max-idle": "Jumlah maksimum koneksi dalam pool koneksi nonaktif. Jika <1>Koneksi terbuka maksimum lebih besar dari 0 tetapi kurang dari <3>Koneksi nonaktif maksimum, maka <5>Koneksi nonaktif maksimum akan dikurangi agar sesuai dengan batas <8>Koneksi terbuka maksimum. Jika diatur ke 0, tidak ada koneksi nonaktif yang dipertahankan.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "Jumlah waktu maksimum dalam detik untuk menggunakan koneksi lagi. Jika diatur ke 0, koneksi digunakan kembali terus-menerus tanpa batas waktu.", - "content-max-open": "Jumlah koneksi terbuka maksimum ke basis data. Jika <1>Koneksi nonaktif maksimum lebih besar dari 0 dan <3>Koneksi terbuka maksimum kurang dari <5>Koneksi nonaktif maksimum, maka <7>Koneksi nonaktif maksimum akan dikurangi agar sesuai dengan batas <9>Koneksi terbuka maksimum. Jika diatur ke 0, tidak ada batasan jumlah koneksi terbuka.", + "content-max-open": "", "max-idle": "Nonaktif maks", "max-lifetime": "Periode maks", "max-open": "Terbuka maks", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Kueri Anda tidak valid. Periksa di bawah ini untuk detailnya. <1>Namun, Anda masih dapat menjalankan kueri ini.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Pembangun", "label-code": "Kode" @@ -76,7 +76,7 @@ "tooltip-format-query": "Format kueri" }, "query-validator": { - "query-will-process": "<0> Kueri ini akan memproses <2>{{bytes}} saat dijalankan.", + "query-will-process": "", "validating-query": "Memvalidasi kueri..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/it-IT/grafana-sql.json b/packages/grafana-sql/src/locales/it-IT/grafana-sql.json index 7197432881f..47e8e35fb00 100644 --- a/packages/grafana-sql/src/locales/it-IT/grafana-sql.json +++ b/packages/grafana-sql/src/locales/it-IT/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Inattività massima automatica", - "content-auto-max-idle": "Se abilitato, imposta automaticamente il numero di <1>Connessioni inattive massime sullo stesso valore di <3> Connessioni aperte massime. Se il numero massimo di connessioni aperte non è impostato, verrà impostato sul valore predefinito ({{defaultMaxIdle}}).", - "content-max-idle": "Il numero massimo di connessioni nel pool di connessioni inattive. Se <1>Connessioni aperte massine è maggiore di 0 ma inferiore a <3>Connessioni inattive massime, le <5>Connessioni inattive massime verranno ridotte per corrispondere al limite di <8>Connessioni aperte massime. Se impostato su 0, non viene mantenuta alcuna connessione inattiva.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "La quantità massima di tempo in secondi in cui una connessione può essere riutilizzata. Se impostato su 0, le connessioni vengono riutilizzate per sempre.", - "content-max-open": "Il numero massimo di connessioni aperte al database. Se <1>Connessioni inattive massime è maggiore di 0 e <3>Connessioni aperte massime è inferiore a <5>Connessioni inattive massime, <7>Connessioni inattive massime verrà ridotto affinché corrisponda al limite di <9>Connessioni aperte massime. Se impostato su 0, non vi è alcun limite al numero di connessioni aperte.", + "content-max-open": "", "max-idle": "Inattive massime", "max-lifetime": "Durata massima", "max-open": "Aperte massime", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Query non valida. Controlla qui sotto per i dettagli. <1>Tuttavia, puoi comunque eseguire questa query.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Builder", "label-code": "Codice" @@ -76,7 +76,7 @@ "tooltip-format-query": "Formato query" }, "query-validator": { - "query-will-process": "<0> Questa query elabora <2>{{bytes}} quando viene eseguita.", + "query-will-process": "", "validating-query": "Convalida della query in corso..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/ja-JP/grafana-sql.json b/packages/grafana-sql/src/locales/ja-JP/grafana-sql.json index 5aaeb7a6783..8ddb12b79d9 100644 --- a/packages/grafana-sql/src/locales/ja-JP/grafana-sql.json +++ b/packages/grafana-sql/src/locales/ja-JP/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "自動最大アイドル", - "content-auto-max-idle": "有効にすると、<1>最大アイドル接続数が<3>最大オープン接続数と同じ値に自動設定されます。最大オープン接続数が設定されていない場合、デフォルト({{defaultMaxIdle}})に設定されます。", - "content-max-idle": "アイドル接続プールの最大接続数。<1>最大オープン接続数が0より大きく<3>最大アイドル接続数より小さい場合、<5>最大アイドル接続数は<8>最大オープン接続数の制限に合わせて削減されます。0に設定すると、アイドル接続は保持されません。", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "接続を再利用できる最大時間(秒)。0に設定すると、接続は永続的に再利用されます。", - "content-max-open": "データベースへの最大オープン接続数。<1>最大アイドル接続数が0より大きく<3>最大オープン接続数が<5>最大アイドル接続数より小さい場合、<7>最大アイドル接続数は<9>最大オープン接続数の制限に合わせて削減されます。0に設定すると、オープン接続数に制限はありません。", + "content-max-open": "", "max-idle": "最大アイドル", "max-lifetime": "最大有効期間", "max-open": "最大オープン", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "クエリが無効です。詳細は下記をご確認ください。<1>ただし、このクエリは引き続き実行できます。", + "content-invalid-query": "", "editor-modes": { "label-builder": "ビルダー", "label-code": "コード" @@ -76,7 +76,7 @@ "tooltip-format-query": "クエリをフォーマット" }, "query-validator": { - "query-will-process": "<0>このクエリは実行時に<2>{{bytes}}を処理します。", + "query-will-process": "", "validating-query": "クエリを検証中..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/ko-KR/grafana-sql.json b/packages/grafana-sql/src/locales/ko-KR/grafana-sql.json index 61248514907..564cb59dc6b 100644 --- a/packages/grafana-sql/src/locales/ko-KR/grafana-sql.json +++ b/packages/grafana-sql/src/locales/ko-KR/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "자동 최대 유휴", - "content-auto-max-idle": "활성화하면, <1>유휴 연결 수 최대 한도를 <3>열린 연결 수 최대 한도와 동일한 값으로 자동 설정합니다. 열린 연결 수 최대 한도를 설정하지 않으면, 기본값({{defaultMaxIdle}})으로 설정됩니다.", - "content-max-idle": "유휴 연결 풀의 연결 최대 한도입니다. <1>열린 연결 최대 한도가 0보다 크지만 <3>유휴 연결 최대 한도보다 작으면, <5>유휴 연결 최대 한도가 <8>열린 연결 최대 한도에 맞춰 감소합니다. 0으로 설정된 경우, 유휴 연결이 유지되지 않습니다.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "연결을 재사용할 수 있는 최대 시간(초)입니다. 0으로 설정하면, 연결이 영구적으로 재사용됩니다.", - "content-max-open": "데이터베이스에 대한 열린 연결 최대 한도입니다. <1>유휴 연결 최대 한도가 0보다 크고 <3>열린 연결 최대 한도가 <5>유휴 연결 최대 한도보다 작으면, <7>유휴 연결 최대 한도가 <9>열린 연결 최대 한도와 일치하도록 감소합니다. 0으로 설정하면, 열린 연결 수에 제한이 없습니다.", + "content-max-open": "", "max-idle": "유휴 최대 한도", "max-lifetime": "최대 수명 기간", "max-open": "열 한도", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "쿼리가 유효하지 않습니다. 자세한 내용은 아래에서 확인하세요. <1>하지만 이 쿼리는 계속 실행할 수 있습니다.", + "content-invalid-query": "", "editor-modes": { "label-builder": "빌더", "label-code": "코드" @@ -76,7 +76,7 @@ "tooltip-format-query": "형식 쿼리" }, "query-validator": { - "query-will-process": "<0> 이 쿼리는 실행 시 <2>{{bytes}}를 처리합니다.", + "query-will-process": "", "validating-query": "쿼리 유효성 검사 중..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/nl-NL/grafana-sql.json b/packages/grafana-sql/src/locales/nl-NL/grafana-sql.json index 4860e9bd054..a13866b5e35 100644 --- a/packages/grafana-sql/src/locales/nl-NL/grafana-sql.json +++ b/packages/grafana-sql/src/locales/nl-NL/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Automatisch maximaal inactief", - "content-auto-max-idle": "Indien ingeschakeld, stel je het aantal <1>Maximale inactieve verbindingen automatisch in op dezelfde waarde als<3> Max open verbindingen. Als het aantal maximale open verbindingen niet is ingesteld, wordt dit ingesteld op de standaardwaarde ({{defaultMaxIdle}}).", - "content-max-idle": "Het maximale aantal verbindingen in de inactieve verbindingspool. Als <1>Max open verbindingen groter is dan 0 maar kleiner dan de <3>Max inactieve verbindingen, dan worden de <5>Max inactieve verbindingen verminderd om overeen te komen met de limiet <8>Max open verbindingen. Indien ingesteld op 0, blijven er geen inactieve verbindingen behouden.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "De maximale hoeveelheid tijd in seconden dat een verbinding opnieuw kan worden gebruikt. Indien ingesteld op 0, worden verbindingen voor altijd hergebruikt.", - "content-max-open": "Het maximale aantal open verbindingen met de database. Als <1>Max inactieve verbindingen groter is dan 0 en de <3>Max open verbindingen kleiner is dan <5>Max inactieve verbindingen, dan wordt <7>Max inactieve verbindingen verminderd om overeen te komen met de limiet <9>Max open verbindingen. Indien ingesteld op 0, is er geen limiet op het aantal open verbindingen.", + "content-max-open": "", "max-idle": "Max. inactief", "max-lifetime": "Max. levensduur", "max-open": "Max. open", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Je query is ongeldig. Kijk hieronder voor meer informatie. <1>Je kunt deze query echter nog steeds uitvoeren.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Bouwer", "label-code": "Code" @@ -76,7 +76,7 @@ "tooltip-format-query": "Query-formaat" }, "query-validator": { - "query-will-process": "<0> Deze query zal <2>{{bytes}} verwerken wanneer deze wordt uitgevoerd.", + "query-will-process": "", "validating-query": "Query valideren..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/pl-PL/grafana-sql.json b/packages/grafana-sql/src/locales/pl-PL/grafana-sql.json index 677637715ce..96338ff59d8 100644 --- a/packages/grafana-sql/src/locales/pl-PL/grafana-sql.json +++ b/packages/grafana-sql/src/locales/pl-PL/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Automatyczny maksymalny czas bezczynności", - "content-auto-max-idle": "Jeśli ta opcja jest włączona, automatycznie ustawia <1>maksymalną liczbę bezczynnych połączeń na tę samą wartość, co <3>maksymalna liczba otwartych połączeń. Jeśli maksymalna liczba otwartych połączeń nie jest określona, zostanie ustawiona wartość domyślna ({{defaultMaxIdle}}).", - "content-max-idle": "Maksymalna liczba połączeń w puli bezczynnych połączeń. Jeśli <1>maksymalna liczba otwartych połączeń jest większa niż 0, ale mniejsza niż <3>maksymalna liczba bezczynnych połączeń, to <5>maksymalna liczba bezczynnych połączeń zostanie zmniejszona, aby dopasować ją do limitu <8>maksymalnej liczby otwartych połączeń. Jeśli ustawiono wartość 0, nie są zachowywane żadne bezczynne połączenia.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "Maksymalny czas w sekundach, przez który połączenie może być ponownie użyte. Jeśli ustawiono wartość 0, połączenia są ponownie wykorzystywane bez daty zakończenia.", - "content-max-open": "Maksymalna liczba otwartych połączeń z bazą danych. Jeśli <1>maksymalna liczba bezczynnych połączeń jest większa niż 0, a <3>maksymalna liczba otwartych połączeń jest mniejsza niż <5>maksymalna liczba bezczynnych połączeń, to <7>maksymalna liczba bezczynnych połączeń zostanie zmniejszona, aby dopasować ją do limitu <9>maksymalnej liczby otwartych połączeń. Jeśli ustawiono wartość 0, liczba otwartych połączeń nie jest ograniczona.", + "content-max-open": "", "max-idle": "Maks. czas bezczynności", "max-lifetime": "Maks. czas istnienia", "max-open": "Maks. czas otwarcia", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Zapytanie jest nieprawidłowe. Szczegóły znajdziesz poniżej. <1>Nadal jednak możesz uruchomić to zapytanie.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Konstruktor", "label-code": "Kod" @@ -76,7 +76,7 @@ "tooltip-format-query": "Formatuj zapytanie" }, "query-validator": { - "query-will-process": "<0> To zapytanie przetworzy <2>{{bytes}} po uruchomieniu.", + "query-will-process": "", "validating-query": "Sprawdzanie poprawności zapytania…" }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/pt-BR/grafana-sql.json b/packages/grafana-sql/src/locales/pt-BR/grafana-sql.json index d897588a416..d36a0b4868a 100644 --- a/packages/grafana-sql/src/locales/pt-BR/grafana-sql.json +++ b/packages/grafana-sql/src/locales/pt-BR/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Inatividade máxima automática", - "content-auto-max-idle": "Se ativado, define automaticamente o número <1>Máximo de conexões ociosas para o mesmo valor que o<3> Máximo de conexões abertas. Se o número máximo de conexões abertas não estiver definido, ele será definido de acordo com o padrão ({{defaultMaxIdle}}).", - "content-max-idle": "O número máximo de conexões no grupo de conexões ociosas. Se o <1>Máximo de conexões abertas for maior que 0, mas menor que o <3>Máximo de conexões ociosas, o <5>Máximo de conexões ociosas será reduzido para corresponder ao limite do <8>Máximo de conexões abertas. Se estiver definido como 0, nenhuma conexão ociosa será mantida.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "A quantidade máxima de tempo em segundos que uma conexão pode ser reutilizada. Se estiver definido como 0, as conexões serão reutilizadas indefinidamente.", - "content-max-open": "O número máximo de conexões abertas com o banco de dados. Se o <1>Máximo de conexões ociosas for maior que 0 e o <3>Máximo de conexões abertas for menor que o <5>Máximo de conexões ociosas, o <7>Máximo de conexões ociosas será reduzido para corresponder ao limite do <9>Máximo de conexões abertas. Se estiver definido como 0, não haverá limite para o número de conexões abertas.", + "content-max-open": "", "max-idle": "Máximo de ociosidade", "max-lifetime": "Tempo de vida máximo", "max-open": "Máximo de aberturas", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Sua consulta é inválida. Confira abaixo para saber mais. <1>No entanto, você ainda pode executar esta consulta.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Construtor", "label-code": "Código" @@ -76,7 +76,7 @@ "tooltip-format-query": "Formatar consulta" }, "query-validator": { - "query-will-process": "<0> Esta consulta processará <2>{{bytes}} ao ser executada.", + "query-will-process": "", "validating-query": "Validando consulta…" }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/pt-PT/grafana-sql.json b/packages/grafana-sql/src/locales/pt-PT/grafana-sql.json index ced6291980a..0f33de74f02 100644 --- a/packages/grafana-sql/src/locales/pt-PT/grafana-sql.json +++ b/packages/grafana-sql/src/locales/pt-PT/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Ralenti máximo automático", - "content-auto-max-idle": "Se ativado, defina automaticamente o número de <1>Ligações de ralenti máximas para o mesmo valor que<3> Ligações abertas máximas. Se o número de ligações abertas máximas não estiver definido, será definido para a predefinição ({{defaultMaxIdle}}).", - "content-max-idle": "O número máximo de ligações no conjunto de ligações de ralenti. Se <1>Ligações abertas máximas for maior que 0, mas menor que as <3>Ligações de ralenti máximas, então as <5>Ligações de ralenti máximas serão reduzidas para corresponder ao limite de <8>Ligações abertas máximas. Se definido para 0, nenhuma ligação de ralenti será retida.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "A quantidade máxima de tempo em segundos que uma ligação pode ser reutilizada. Se definido para 0, as ligações serão reutilizadas para sempre.", - "content-max-open": "O número máximo de ligações abertas à base de dados. Se <1>Ligações de ralenti máximas for maior que 0 e as <3>Ligações abertas máximas for menor que <5>Ligações de ralenti máximas, então <7>Ligações de ralenti máximas serão reduzidas para corresponder ao limite de <9>Ligações abertas máximas. Se definido para 0, não há limite para o número de ligações abertas.", + "content-max-open": "", "max-idle": "Ralenti máx.", "max-lifetime": "Tempo de vida máximo", "max-open": "Aberturas máx.", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "A sua consulta é inválida. Verifique abaixo para obter detalhes. <1>No entanto, ainda pode executar esta consulta.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Construtor", "label-code": "Cód." @@ -76,7 +76,7 @@ "tooltip-format-query": "Formatar consulta" }, "query-validator": { - "query-will-process": "<0> Esta consulta processará <2>{{bytes}} quando for executada.", + "query-will-process": "", "validating-query": "A validar consulta..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/ru-RU/grafana-sql.json b/packages/grafana-sql/src/locales/ru-RU/grafana-sql.json index 3e71d2e580d..00b5a7fdd64 100644 --- a/packages/grafana-sql/src/locales/ru-RU/grafana-sql.json +++ b/packages/grafana-sql/src/locales/ru-RU/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Автоматический максимум неактивных подключений", - "content-auto-max-idle": "Если этот параметр включен, параметр <1>Макс. кол-во неактивных подключений автоматически устанавливается на значение параметра <3> Макс. кол-во открытых подключений. Если максимальное количество открытых подключений не установлено, используется значение по умолчанию ({{defaultMaxIdle}}).", - "content-max-idle": "Максимальное количество подключений в пуле неактивных подключений. Если значение параметра <1>Макс. кол-во открытых подключений больше 0, но меньше значения <3>Макс. кол-во неактивных подключений, то значение <5>Макс. кол-во неактивных подключений уменьшается, чтобы соответствовать предельному значению параметра <8>Макс. кол-во открытых подключений. Если установлено значение 0, неактивные подключения не сохраняются.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "Максимальное время (в секундах), в течение которого подключение может быть использовано повторно. Если установлено значение 0, подключения всегда используются повторно.", - "content-max-open": "Максимальное количество открытых подключений к базе данных. Если значение параметра <1>Макс. кол-во неактивных подключений больше 0, а значение параметра <3>Макс. кол-во открытых подключений меньше значения <5>Макс. кол-во неактивных подключений, то значение <7>Макс. кол-во неактивных подключений уменьшается, чтобы соответствовать предельному значению параметра <9>Макс. кол-во открытых подключений. Если установлено значение 0, количество открытых подключений не ограничено.", + "content-max-open": "", "max-idle": "Макс. кол-во неактивных подключений", "max-lifetime": "Макс. время жизни", "max-open": "Макс. кол-во открытых подключений", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Недействительный запрос. См. подробности ниже. <1>Однако вы всё равно можете выполнить этот запрос.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Конструктор", "label-code": "Код" @@ -76,7 +76,7 @@ "tooltip-format-query": "Форматировать запрос" }, "query-validator": { - "query-will-process": "<0> При выполнении запрос обработает <2>{{bytes}}.", + "query-will-process": "", "validating-query": "Проверка запроса..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/sv-SE/grafana-sql.json b/packages/grafana-sql/src/locales/sv-SE/grafana-sql.json index 9533bd6afc4..b334629c2cb 100644 --- a/packages/grafana-sql/src/locales/sv-SE/grafana-sql.json +++ b/packages/grafana-sql/src/locales/sv-SE/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Automatisk max. inaktiv", - "content-auto-max-idle": "Om det är aktiverat ställer du automatiskt in antalet <1>Maximalt antal inaktiva anslutningar till samma värde som<3> Max öppna anslutningar. Om antalet maximala öppna anslutningar inte är inställt kommer det att ställas in på standard ({{defaultMaxIdle}}).", - "content-max-idle": "Det maximala antalet anslutningar i den inaktiva anslutningspoolen. Om <1>Max öppna anslutningar är större än 0 men mindre än <3>Max inaktiva anslutningar kommer <5>Max inaktiva anslutningar att reduceras för att matcha gränsen för <8>Max öppna anslutningar. Om inställningen är 0 behålls inga inaktiva anslutningar.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "Den maximala tiden i sekunder som en anslutning kan återanvändas. Om inställningen är 0 återanvänds anslutningarna för evigt.", - "content-max-open": "Det maximala antalet öppna anslutningar till databasen. Om <1>Max inaktiva anslutningar är större än 0 och <3>Max öppna anslutningar är mindre än <5>Max inaktiva anslutningar kommer <7>Max inaktiva anslutningar att reduceras för att matcha gränsen för <9>Max öppna anslutningar. Om inställningen är 0 finns det ingen gräns för antalet öppna anslutningar.", + "content-max-open": "", "max-idle": "Max inaktiv", "max-lifetime": "Max livstid", "max-open": "Max öppen", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Din fråga är ogiltig. Se nedan för mer information. <1>Du kan däremot fortfarande köra denna fråga.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Builder", "label-code": "Kod" @@ -76,7 +76,7 @@ "tooltip-format-query": "Formatera fråga" }, "query-validator": { - "query-will-process": "<0> Den här frågan kommer att behandla <2>{{bytes}} när den körs.", + "query-will-process": "", "validating-query": "Validerar frågan …" }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/tr-TR/grafana-sql.json b/packages/grafana-sql/src/locales/tr-TR/grafana-sql.json index 873565bc5d1..79a6c8bf2dd 100644 --- a/packages/grafana-sql/src/locales/tr-TR/grafana-sql.json +++ b/packages/grafana-sql/src/locales/tr-TR/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "Otomatik maksimum boşta bağlantı sayısı", - "content-auto-max-idle": "Etkinleştirilirse <1>Maksimum boşta bağlantı sayısı, <3>Maksimum açık bağlantı sayısıyla aynı değere otomatik olarak ayarlanır. Maksimum açık bağlantı sayısı ayarlanmamışsa varsayılan değere ayarlanır ({{defaultMaxIdle}}).", - "content-max-idle": "Bu, boşta bağlantı havuzundaki maksimum bağlantı sayısını belirtir. <1>Maksimum açık bağlantı değeri 0'dan büyük ancak <3>Maksimum boşta bağlantı değerinden küçükse <5>Maksimum boşta bağlantı sayısı, <8>Maksimum açık bağlantı sınırına uyacak şekilde azaltılır. 0 olarak ayarlanırsa boşta bağlantılar tutulmaz.", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "Saniye cinsinden bir bağlantının yeniden kullanılabileceği maksimum süre. 0 olarak ayarlanırsa bağlantılar süresiz olarak yeniden kullanılır.", - "content-max-open": "Veri tabanına açılabilecek maksimum açık bağlantı sayısı. <1>Maksimum boşta bağlantı sayısı 0'dan büyük ve <3>Maksimum açık bağlantı sayısı <5>Maksimum boşta bağlantı sayısından küçükse <7>Maksimum boşta bağlantı değeri, <9>Maksimum açık bağlantı sınırına uyacak şekilde azaltılır. 0 olarak ayarlanırsa açık bağlantı sayısı için bir sınır yoktur.", + "content-max-open": "", "max-idle": "Maks. boşta", "max-lifetime": "Maksimum kullanım süresi", "max-open": "Maks. açık", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Sorgunuz geçersiz. Ayrıntılar için aşağıya bakın. <1>Ancak yine de bu sorguyu çalıştırabilirsiniz.", + "content-invalid-query": "", "editor-modes": { "label-builder": "Oluşturucu", "label-code": "Kod" @@ -76,7 +76,7 @@ "tooltip-format-query": "Sorguyu biçimlendir" }, "query-validator": { - "query-will-process": "<0> Bu sorgu çalıştırıldığında <2>{{bytes}} işlenecek.", + "query-will-process": "", "validating-query": "Sorgu doğrulanıyor..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/zh-Hans/grafana-sql.json b/packages/grafana-sql/src/locales/zh-Hans/grafana-sql.json index 296562625e3..0bc830cb365 100644 --- a/packages/grafana-sql/src/locales/zh-Hans/grafana-sql.json +++ b/packages/grafana-sql/src/locales/zh-Hans/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "自动最大空闲", - "content-auto-max-idle": "如果启用,则自动将<1>最大空闲连接数的数字设置为与<3>最大打开连接数相同的值。如果未设置最大打开连接数的数字,则将其设置为默认值 ({{defaultMaxIdle}})。", - "content-max-idle": "空闲连接池中的最大连接数。如果<1>最大打开连接数大于 0 但小于<3>最大空闲连接数,则<5>最大空闲连接数将减少以匹配<8>最大打开连接数限制。如果设置为 0,则不保留任何空闲连接。", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "连接可以重复使用的最长时间(秒)。如果设置为 0,则连接将永久重复使用。", - "content-max-open": "数据库的最大打开连接数。如果<1>最大空闲连接数大于 0 且<3>最大打开连接数小于<5>最大空闲连接数,则<7>最大空闲连接数将减少以匹配<9>最大打开连接数限制。如果设置为 0,则对打开的连接数没有限制。", + "content-max-open": "", "max-idle": "最大空闲", "max-lifetime": "最大生命周期", "max-open": "最大打开", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "您的查询无效。请查看下方详情。<1>但是,您仍然可以运行此查询。", + "content-invalid-query": "", "editor-modes": { "label-builder": "构建器", "label-code": "代码" @@ -76,7 +76,7 @@ "tooltip-format-query": "格式化查询" }, "query-validator": { - "query-will-process": "<0> 此查询将在运行时处理 <2>{{bytes}}。", + "query-will-process": "", "validating-query": "正在验证查询..." }, "raw-editor": { diff --git a/packages/grafana-sql/src/locales/zh-Hant/grafana-sql.json b/packages/grafana-sql/src/locales/zh-Hant/grafana-sql.json index f09f0f3dcfd..e093a0c927f 100644 --- a/packages/grafana-sql/src/locales/zh-Hant/grafana-sql.json +++ b/packages/grafana-sql/src/locales/zh-Hant/grafana-sql.json @@ -11,10 +11,10 @@ }, "connection-limits": { "auto-max-idle": "自動最大閒置時間", - "content-auto-max-idle": "若啟用,<1>最大閒置連線數的數量將會自動設定為與<3>最大開啟連線數相同的數值。如果未設定最大開啟連線數,則將設定為預設值 ({{defaultMaxIdle}})。", - "content-max-idle": "閒置連線池中的最大連線數。若<1>最大開啟連線數大於 0,但小於 <3>最大閒置連線數,則<5>最大閒置連線數將會縮減,以符合<8>最大開啟連線數限制。若設定為 0,則不會保留任何閒置連線。", + "content-auto-max-idle": "", + "content-max-idle": "", "content-max-lifetime": "可以重複使用連線的最長時間(秒)。若設定為 0,則會永遠重複使用連線。", - "content-max-open": "資料庫的最大開啟連線數。若<1>最大閒置連線數大於 0,且<3>最大開啟連線數小於<5>最大閒置連線數,則<7>最大閒置連線數將會縮減,以符合<9>最大開啟連線數限制。若設定為 0,則不會限制開啟連線數。", + "content-max-open": "", "max-idle": "最大閒置", "max-lifetime": "最大存活期", "max-open": "最大開啟數量", @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "您的查詢無效。請參閱下方詳細資訊。<1>不過,您仍然可以執行此查詢。", + "content-invalid-query": "", "editor-modes": { "label-builder": "建立器", "label-code": "代碼" @@ -76,7 +76,7 @@ "tooltip-format-query": "格式化查詢" }, "query-validator": { - "query-will-process": "<0>此查詢將在執行時處理 <2>{{bytes}}。", + "query-will-process": "", "validating-query": "正在驗證查詢…" }, "raw-editor": { diff --git a/packages/grafana-test-utils/package.json b/packages/grafana-test-utils/package.json index 70bf836f843..a8b69f8d665 100644 --- a/packages/grafana-test-utils/package.json +++ b/packages/grafana-test-utils/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/test-utils", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "private": true, "description": "Grafana test utils & Mock API", "keywords": [ @@ -65,7 +65,7 @@ "@types/chance": "^1.1.7", "@types/jest": "29.5.14", "@types/lodash": "4.17.20", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "jest": "29.7.0", "typescript": "5.9.2" } diff --git a/packages/grafana-test-utils/src/handlers/api/search/handlers.ts b/packages/grafana-test-utils/src/handlers/api/search/handlers.ts index 2aebd03b174..d9fa090fbf1 100644 --- a/packages/grafana-test-utils/src/handlers/api/search/handlers.ts +++ b/packages/grafana-test-utils/src/handlers/api/search/handlers.ts @@ -17,12 +17,17 @@ const slugify = (str: string) => { .replace(/ +/g, '-'); }; +const typeFilterMap: Record = { + 'dash-db': 'dashboard', + 'dash-folder': 'folder', +}; + const getLegacySearchHandler = () => http.get('/api/search', ({ request }) => { const folderFilter = new URL(request.url).searchParams.get('folderUIDs') || null; const typeFilter = new URL(request.url).searchParams.get('type') || null; // Workaround for the fixture kind being 'dashboard' instead of 'dash-db' - const mappedTypeFilter = typeFilter === 'dash-db' ? 'dashboard' : typeFilter; + const mappedTypeFilter = typeFilter ? typeFilterMap[typeFilter] || typeFilter : null; const starredFilter = new URL(request.url).searchParams.get('starred') || null; const tagFilter = new URL(request.url).searchParams.getAll('tag') || null; diff --git a/packages/grafana-test-utils/src/handlers/apis/dashboard.grafana.app/v1beta1/handlers.ts b/packages/grafana-test-utils/src/handlers/apis/dashboard.grafana.app/v1beta1/handlers.ts index 0c9c364780d..251ca44e766 100644 --- a/packages/grafana-test-utils/src/handlers/apis/dashboard.grafana.app/v1beta1/handlers.ts +++ b/packages/grafana-test-utils/src/handlers/apis/dashboard.grafana.app/v1beta1/handlers.ts @@ -27,6 +27,7 @@ const dashboardToAppPlatform = (dashboard: (typeof mockTree)[number]['item']) => }, status: {}, // TODO: Eventually add access properties, as required by tests + access: {}, }; }; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 91dc005eadc..011a26fb86d 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "12.3.0-pre", + "version": "12.4.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -67,11 +67,11 @@ "@emotion/react": "11.14.0", "@emotion/serialize": "1.3.3", "@floating-ui/react": "0.27.16", - "@grafana/data": "12.3.0-pre", - "@grafana/e2e-selectors": "12.3.0-pre", + "@grafana/data": "12.4.0-pre", + "@grafana/e2e-selectors": "12.4.0-pre", "@grafana/faro-web-sdk": "^1.13.2", - "@grafana/i18n": "12.3.0-pre", - "@grafana/schema": "12.3.0-pre", + "@grafana/i18n": "12.4.0-pre", + "@grafana/schema": "12.4.0-pre", "@hello-pangea/dnd": "18.0.1", "@monaco-editor/react": "4.7.0", "@popperjs/core": "2.11.8", @@ -165,7 +165,7 @@ "@types/is-hotkey": "0.1.10", "@types/jest": "29.5.14", "@types/mock-raf": "1.0.6", - "@types/node": "22.17.0", + "@types/node": "24.9.2", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-color": "3.0.13", diff --git a/packages/grafana-ui/src/components/Alert/Alert.tsx b/packages/grafana-ui/src/components/Alert/Alert.tsx index 56b81d14351..8b3b55a41cc 100644 --- a/packages/grafana-ui/src/components/Alert/Alert.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.tsx @@ -26,6 +26,11 @@ export interface Props extends HTMLAttributes { topSpacing?: number; } +/** + * An alert displays an important message in a way that attracts the user's attention without interrupting the user's task. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-alert--docs + */ export const Alert = React.forwardRef( ( { diff --git a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx index f53670b73a6..f78262d0548 100644 --- a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx +++ b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.tsx @@ -21,6 +21,12 @@ export interface Props extends Omit { /** Input that will save its value on change */ children: (onChange: (newValue: T) => void) => React.ReactElement; } + +/** + * Used for form inputs that should save its content automatically. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-autosavefield--docs + */ export function AutoSaveField(props: Props) { const { invalid, diff --git a/packages/grafana-ui/src/components/Badge/Badge.tsx b/packages/grafana-ui/src/components/Badge/Badge.tsx index 201957ab534..53bcabc646d 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.tsx +++ b/packages/grafana-ui/src/components/Badge/Badge.tsx @@ -47,6 +47,11 @@ const BadgeSkeleton: SkeletonComponent = ({ rootProps }) => { return ; }; +/** + * The badge component adds meta information to other content, for example about release status or new elements. You can add any `Icon` component or use the badge without an icon. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-badge--docs + */ export const Badge = attachSkeleton(BadgeComponent, BadgeSkeleton); const getSkeletonStyles = () => ({ diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 3392f059662..639bf41ca13 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -56,6 +56,9 @@ export interface Props extends Themeable2 { isOverflow: boolean; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-bargauge--docs + */ export class BarGauge extends PureComponent { static defaultProps: Partial = { lcdCellWidth: 12, diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.tsx index 08bacf3e2af..870d350ba95 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValue.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValue.tsx @@ -82,6 +82,11 @@ export interface Props extends Themeable2 { disableWideLayout?: boolean; } +/** + * Component for showing a value based on a [DisplayValue](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/displayValue.ts#L5). + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-bigvalue--docs + */ export const BigValue = memo((props) => { const { onClick, className, hasLinks, theme, justifyMode = BigValueJustifyMode.Auto } = props; diff --git a/packages/grafana-ui/src/components/Button/Button.test.tsx b/packages/grafana-ui/src/components/Button/Button.test.tsx index 52589d67196..b78b9aafe5d 100644 --- a/packages/grafana-ui/src/components/Button/Button.test.tsx +++ b/packages/grafana-ui/src/components/Button/Button.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Button } from './Button'; +import { Button, LinkButton } from './Button'; const setup = (jsx: JSX.Element) => { return { @@ -64,3 +64,37 @@ describe('Button', () => { expect(screen.getByRole('button', { name: 'Aria label' })).toBeInTheDocument(); }); }); + +describe('LinkButton', () => { + it('should place the icon on the right when iconPlacement is "right"', () => { + setup( + + Click me + + ); + + const link = screen.getByRole('link'); + const icon = screen.getByTitle(''); + const textSpan = link.querySelector('span'); + + // Assert that the text span comes before the icon in the DOM + expect(link.childNodes[0]).toBe(textSpan); + expect(link.childNodes[1]).toBe(icon); + }); + + it('should place the icon on the left when iconPlacement is "left"', () => { + setup( + + Click me + + ); + + const link = screen.getByRole('link'); + const icon = screen.getByTitle(''); + const textSpan = link.querySelector('span'); + + // Assert that the icon comes before the text span in the DOM + expect(link.childNodes[0]).toBe(icon); + expect(link.childNodes[1]).toBe(textSpan); + }); +}); diff --git a/packages/grafana-ui/src/components/Button/Button.tsx b/packages/grafana-ui/src/components/Button/Button.tsx index c596904a39b..d8e6e54d3fa 100644 --- a/packages/grafana-ui/src/components/Button/Button.tsx +++ b/packages/grafana-ui/src/components/Button/Button.tsx @@ -51,6 +51,9 @@ type CommonProps = BasePropsWithChildren | NoChildrenTooltip | NoChildrenAriaLab export type ButtonProps = CommonProps & ButtonHTMLAttributes; +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-button--docs + */ export const Button = React.forwardRef( ( { @@ -139,6 +142,7 @@ export const LinkButton = React.forwardRef( size = 'md', fill = 'solid', icon, + iconPlacement = 'left', fullWidth, children, className, @@ -171,6 +175,8 @@ export const LinkButton = React.forwardRef( className ); + const iconComponent = icon && ; + // When using tooltip, ref is forwarded to Tooltip component instead for https://github.com/grafana/grafana/issues/65632 const button = ( ( ref={tooltip ? undefined : ref} aria-label={ariaLabel ?? (!children && typeof tooltip === 'string' ? tooltip : undefined)} > - + {iconPlacement === 'left' && iconComponent} {children && {children}} + {iconPlacement === 'right' && iconComponent} ); diff --git a/packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.tsx b/packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.tsx index 43aaff2bee2..e52b368f759 100644 --- a/packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.tsx +++ b/packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.tsx @@ -28,6 +28,9 @@ export interface ButtonCascaderProps { hideDownIcon?: boolean; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-buttoncascader--docs + */ export const ButtonCascader = (props: ButtonCascaderProps) => { const { onChange, className, loadData, icon, buttonProps, hideDownIcon, variant, disabled, ...rest } = props; const styles = useStyles2(getStyles); diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx index ca3348c2b54..cd859f77256 100644 --- a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx @@ -11,7 +11,11 @@ export interface CallToActionCardProps { className?: string; } -/** @deprecated Use instead */ +/** + * @deprecated Use `` instead. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-deprecated-calltoactioncard--docs + */ export const CallToActionCard = ({ message, callToActionElement, footer, className }: CallToActionCardProps) => { const css = useStyles2(getStyles); diff --git a/packages/grafana-ui/src/components/Card/Card.tsx b/packages/grafana-ui/src/components/Card/Card.tsx index 80194b56136..f1514f6f60e 100644 --- a/packages/grafana-ui/src/components/Card/Card.tsx +++ b/packages/grafana-ui/src/components/Card/Card.tsx @@ -51,6 +51,7 @@ const CardContext = React.createContext<{ /** * Generic card component * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-card--docs * @public */ export const Card: CardInterface = ({ diff --git a/packages/grafana-ui/src/components/Card/CardContainer.tsx b/packages/grafana-ui/src/components/Card/CardContainer.tsx index 270ac37931c..f970356ec80 100644 --- a/packages/grafana-ui/src/components/Card/CardContainer.tsx +++ b/packages/grafana-ui/src/components/Card/CardContainer.tsx @@ -93,7 +93,7 @@ export const getCardContainerStyles = ( display: 'grid', position: 'relative', gridTemplateColumns: 'auto 1fr auto', - gridTemplateRows: '1fr auto auto auto', + gridTemplateRows: 'auto auto 1fr auto', gridAutoColumns: '1fr', gridAutoFlow: 'row', gridTemplateAreas: ` diff --git a/packages/grafana-ui/src/components/Carousel/Carousel.tsx b/packages/grafana-ui/src/components/Carousel/Carousel.tsx index f3cc7892293..57f4ed59005 100644 --- a/packages/grafana-ui/src/components/Carousel/Carousel.tsx +++ b/packages/grafana-ui/src/components/Carousel/Carousel.tsx @@ -22,6 +22,11 @@ export interface CarouselProps { images: CarouselImage[]; } +/** + * The Carousel component displays a grid of image thumbnails that can be clicked to view full-sized images in a modal with navigation controls. It provides an elegant way to present collections of images or screenshots with fullscreen preview capabilities. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-carousel--docs + */ export const Carousel: React.FC = ({ images }) => { const [selectedIndex, setSelectedIndex] = useState(null); const [imageErrors, setImageErrors] = useState>({}); diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.tsx index ed8c1af953d..8f80b3b6093 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.tsx @@ -321,4 +321,9 @@ class UnthemedCascader extends PureComponent { } } +/** + * The cascader component is a Select with a cascading flyout menu. When you have lots of options in your select, they can be hard to navigate from a regular dropdown list. In that case you can use the cascader to organize your options into groups hierarchically. Just like in the Select component, the cascader input doubles as a search field to quickly jump to a selection without navigating the list. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-cascader--docs + */ export const Cascader = withTheme2(UnthemedCascader); diff --git a/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx b/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx index 4a3b9e9b88a..1e9ab3e6de1 100644 --- a/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx +++ b/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx @@ -13,6 +13,11 @@ export interface Props { children: React.ReactNode; } +/** + * A wrapper component that detects clicks outside of the elements by attaching event listener to `window` or `document` objects. Useful for components that require an action being triggered when a click outside has occurred, for example closing an overlay or popup. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/utilities-clickoutsidewrapper--docs + */ export function ClickOutsideWrapper({ includeButtonPress = true, parent = window, diff --git a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx index a42871dcde0..f1f00cdb25e 100644 --- a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx +++ b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx @@ -21,6 +21,11 @@ export type Props = ButtonProps & { const SHOW_SUCCESS_DURATION = 2 * 1000; +/** + * A control for allowing the user to copy text to their clipboard. Uses native APIs on modern browsers, falling back to the old `document.execCommand('copy')` API on other browsers. The text to be copied should be provided via `getText` prop. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-clipboardbutton--docs + */ export function ClipboardButton({ onClipboardCopy, onClipboardError, diff --git a/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx b/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx index 12c00333dfd..936d9837813 100644 --- a/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx +++ b/packages/grafana-ui/src/components/Collapse/CollapsableSection.tsx @@ -25,6 +25,11 @@ export interface Props { unmountContentWhenClosed?: boolean; } +/** + * A simple container for enabling collapsing/expanding of content. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-collapsablesection--docs + */ export const CollapsableSection = ({ label, isOpen, diff --git a/packages/grafana-ui/src/components/Collapse/Collapse.story.tsx b/packages/grafana-ui/src/components/Collapse/Collapse.story.tsx index 002a6f00093..23eda8f784e 100644 --- a/packages/grafana-ui/src/components/Collapse/Collapse.story.tsx +++ b/packages/grafana-ui/src/components/Collapse/Collapse.story.tsx @@ -2,6 +2,9 @@ import { action } from '@storybook/addon-actions'; import { useArgs } from '@storybook/preview-api'; import { Meta, StoryFn } from '@storybook/react'; +import { IconButton } from '../IconButton/IconButton'; +import { Stack } from '../Layout/Stack/Stack'; + import { Collapse, ControlledCollapse } from './Collapse'; import mdx from './Collapse.mdx'; @@ -22,7 +25,6 @@ const meta: Meta = { children: 'Panel data', isOpen: false, label: 'Collapse panel', - collapsible: true, }, argTypes: { onToggle: { action: 'toggled' }, @@ -57,4 +59,39 @@ Controlled.parameters = { }, }; +export const WithCustomLabel: StoryFn = (args) => { + const [, updateArgs] = useArgs(); + return ( + { + action('onToggle')({ isOpen: !args.isOpen }); + updateArgs({ isOpen: !args.isOpen }); + }} + label={ + + Collapse panel + + { + event.stopPropagation(); + action('onDeleteClick')(); + }} + aria-label="Delete" + name="trash-alt" + /> + + + } + > +

{args.children}

+
+ ); +}; +WithCustomLabel.parameters = { + controls: { + exclude: [...EXCLUDED_PROPS, 'label'], + }, +}; + export default meta; diff --git a/packages/grafana-ui/src/components/Collapse/Collapse.test.tsx b/packages/grafana-ui/src/components/Collapse/Collapse.test.tsx index 432ff8ef56e..817df10153d 100644 --- a/packages/grafana-ui/src/components/Collapse/Collapse.test.tsx +++ b/packages/grafana-ui/src/components/Collapse/Collapse.test.tsx @@ -42,7 +42,7 @@ describe('Collapse', () => { const onToggle = jest.fn(); const { user } = setup( - +
{contentText}
); diff --git a/packages/grafana-ui/src/components/Collapse/Collapse.tsx b/packages/grafana-ui/src/components/Collapse/Collapse.tsx index 57aec7a3ed4..195ea24b4d1 100644 --- a/packages/grafana-ui/src/components/Collapse/Collapse.tsx +++ b/packages/grafana-ui/src/components/Collapse/Collapse.tsx @@ -1,12 +1,11 @@ import { css, cx } from '@emotion/css'; -import { useState } from 'react'; +import { useId, useState } from 'react'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; -import { clearButtonStyles } from '../Button/Button'; -import { Icon } from '../Icon/Icon'; +import { IconButton } from '../IconButton/IconButton'; const getStyles = (theme: GrafanaTheme2) => ({ collapse: css({ @@ -74,25 +73,21 @@ const getStyles = (theme: GrafanaTheme2) => ({ }, }), header: css({ + cursor: 'pointer', label: 'collapse__header', - padding: theme.spacing(1, 2, 1, 2), + padding: theme.spacing(1), display: 'flex', + gap: theme.spacing(1), }), - headerCollapsed: css({ - label: 'collapse__header--collapsed', - padding: theme.spacing(1, 2, 1, 2), + button: css({ + marginRight: 0, }), headerLabel: css({ label: 'collapse__header-label', fontWeight: theme.typography.fontWeightMedium, - marginRight: theme.spacing(1), fontSize: theme.typography.size.md, display: 'flex', - flex: '0 0 100%', - }), - icon: css({ - label: 'collapse__icon', - margin: theme.spacing(0.25, 1, 0, -1), + flex: 1, }), }); @@ -103,12 +98,12 @@ export interface Props { label: React.ReactNode; /** Indicates loading state of the content */ loading?: boolean; - /** Toggle collapsed header icon */ - collapsible?: boolean; /** Callback for the toggle functionality */ onToggle?: (isOpen: boolean) => void; /** Additional class name for the root element */ className?: string; + /** @deprecated this prop is no longer used and will be removed in Grafana 13 */ + collapsible?: boolean; } export const ControlledCollapse = ({ isOpen, onToggle, ...otherProps }: React.PropsWithChildren) => { @@ -116,7 +111,6 @@ export const ControlledCollapse = ({ isOpen, onToggle, ...otherProps }: React.Pr return ( { setOpen(!open); @@ -128,35 +122,43 @@ export const ControlledCollapse = ({ isOpen, onToggle, ...otherProps }: React.Pr ); }; -export const Collapse = ({ - isOpen, - label, - loading, - collapsible, - onToggle, - className, - children, -}: React.PropsWithChildren) => { - const buttonStyles = useStyles2(clearButtonStyles); +/** + * A content area, which can be horizontally collapsed and expanded. Can be used to hide extra information on the page. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-collapse--docs + */ +export const Collapse = ({ isOpen, label, loading, onToggle, className, children }: React.PropsWithChildren) => { const style = useStyles2(getStyles); + const labelId = useId(); + const contentId = useId(); + const onClickToggle = () => { if (onToggle) { onToggle(!isOpen); } }; - const panelClass = cx([style.collapse, className]); - const loaderClass = loading ? cx([style.loader, style.loaderActive]) : cx([style.loader]); - const headerClass = collapsible ? cx([style.header]) : cx([style.headerCollapsed]); + const loaderClass = loading ? cx([style.loader, style.loaderActive]) : style.loader; return (
- + {/* the inner button handles keyboard a11y. this is a convenience for mouse users */} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+ +
+ {label} +
+
{isOpen && ( -
+
{children}
diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 819d60746db..f44b3f00fa4 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -84,6 +84,9 @@ export const colorPickerFactory = ( }; }; +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-colorpicker--docs + */ export const ColorPicker = withTheme2(colorPickerFactory(ColorPickerPopover, 'ColorPicker')); export const SeriesColorPicker = withTheme2(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker')); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx index 27f807051a8..5dd4e2acb25 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.tsx @@ -19,6 +19,9 @@ export interface ColorPickerInputProps extends Omit( ({ value = '', onChange, returnColorAs = 'rgb', ...inputProps }, ref) => { const [currentColor, setColor] = useState(value); diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 2c0350d9329..6e38fa3148e 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -12,6 +12,9 @@ export interface SeriesColorPickerPopoverProps extends ColorPickerProps, Popover onToggleAxis?: () => void; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-seriescolorpicker--docs + */ export const SeriesColorPickerPopover = (props: SeriesColorPickerPopoverProps) => { const { yaxis, onToggleAxis, color, ...colorPickerProps } = props; const yAxisLabel = t('grafana-ui.series-color-picker-popover.y-axis-usage', 'Use right y-axis'); diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index 537e26df1c9..b99e58a447b 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -6,6 +6,7 @@ import { Field } from '../Forms/Field'; import { Combobox } from './Combobox'; import { ComboboxOption } from './types'; +import { DEBOUNCE_TIME_MS } from './useOptions'; // Mock data for the Combobox options const options: ComboboxOption[] = [ @@ -613,6 +614,23 @@ describe('Combobox', () => { expect(onChangeHandler).not.toHaveBeenCalled(); expect(input).toHaveValue('Option 1'); }); + + it('shows loading message', async () => { + const loadingMessage = 'Loading options...'; + const asyncOptions = jest.fn(() => Promise.resolve(simpleAsyncOptions)); + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + await act(async () => jest.advanceTimersByTime(0)); + + expect(await screen.findByText(loadingMessage)).toBeInTheDocument(); + + await act(async () => jest.advanceTimersByTime(DEBOUNCE_TIME_MS)); + + expect(screen.queryByText(loadingMessage)).not.toBeInTheDocument(); + }); }); }); diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index b81d0e601fa..a4664c62441 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -1,7 +1,7 @@ import { cx } from '@emotion/css'; import { useVirtualizer, type Range } from '@tanstack/react-virtual'; import { useCombobox } from 'downshift'; -import React, { useCallback, useId, useMemo } from 'react'; +import React, { ComponentProps, useCallback, useId, useMemo } from 'react'; import { t } from '@grafana/i18n'; @@ -60,6 +60,11 @@ interface ComboboxStaticProps * Called when the input loses focus. */ onBlur?: () => void; + + /** + * Icon to display at the start of the ComboBox input + */ + prefixIcon?: ComponentProps['name']; } interface ClearableProps { @@ -114,8 +119,10 @@ const noop = () => {}; export const VIRTUAL_OVERSCAN_ITEMS = 4; /** - * A performant Select replacement. + * A performant and accessible combobox component that supports both synchronous and asynchronous options loading. It provides type-ahead filtering, keyboard navigation, and virtual scrolling for handling large datasets efficiently. + * Replaces the Select component, and has better performance. * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-combobox--docs * @alpha */ export const Combobox = (props: ComboboxProps) => { @@ -137,6 +144,7 @@ export const Combobox = (props: ComboboxProps) => disabled, portalContainer, invalid, + prefixIcon, } = props; // Value can be an actual scalar Value (string or number), or an Option (value + label), so @@ -376,6 +384,7 @@ export const Combobox = (props: ComboboxProps) => {...(isAutoSize ? { minWidth, maxWidth } : {})} autoFocus={autoFocus} onBlur={onBlur} + prefix={prefixIcon && } disabled={disabled} invalid={invalid} className={styles.input} @@ -402,6 +411,7 @@ export const Combobox = (props: ComboboxProps) => > {isOpen && ( { enableAllOption?: boolean; isMultiSelect?: boolean; error?: boolean; + loading?: boolean; } export const ComboboxList = ({ @@ -34,6 +35,7 @@ export const ComboboxList = ({ enableAllOption, isMultiSelect = false, error = false, + loading = false, }: ComboboxListProps) => { const styles = useStyles2(getComboboxStyles); @@ -161,7 +163,8 @@ export const ComboboxList = ({
{error && } - {options.length === 0 && !error && } + {!loading && options.length === 0 && !error && } + {loading && options.length === 0 && }
); diff --git a/packages/grafana-ui/src/components/Combobox/MessageRows.tsx b/packages/grafana-ui/src/components/Combobox/MessageRows.tsx index a2d45162c01..46f2a5e4cc3 100644 --- a/packages/grafana-ui/src/components/Combobox/MessageRows.tsx +++ b/packages/grafana-ui/src/components/Combobox/MessageRows.tsx @@ -22,6 +22,12 @@ export const NotFoundError = () => ( ); +export const LoadingOptions = () => ( + + Loading options... + +); + const MessageRow = ({ children }: { children: ReactNode }) => { return ( diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx index b3794bf3fa9..581cb6223bf 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx @@ -1,7 +1,7 @@ import { action } from '@storybook/addon-actions'; import { useArgs, useEffect, useState } from '@storybook/preview-api'; import type { Meta, StoryFn, StoryObj } from '@storybook/react'; -import { ComponentProps } from 'react'; +import { ComponentProps, useId } from 'react'; import { Field } from '../Forms/Field'; @@ -17,8 +17,6 @@ const meta: Meta = { docs: { page: mdx, }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, }; @@ -48,22 +46,47 @@ type ManyOptionsArgs = storyArgs & { numberOfOptions?: number }; type Story = StoryObj; -export const Basic: Story = { - args: commonArgs, - render: (args) => { - const [{ value }, setArgs] = useArgs(); +const BasicStory: StoryFn = (args) => { + const [{ value }, setArgs] = useArgs(); + const comboboxId = useId(); - return ( + return ( + { onChangeAction(val); setArgs({ value: val }); }} /> - ); - }, + + ); +}; + +export const Basic: Story = { + args: commonArgs, + render: BasicStory, +}; + +const WithInfoOptionStory: StoryFn = (args) => { + const [{ value }, setArgs] = useArgs(); + const comboboxId = useId(); + + return ( + + { + onChangeAction(val); + setArgs({ value: val }); + }} + /> + + ); }; export const WithInfoOption: Story = { @@ -75,44 +98,37 @@ export const WithInfoOption: Story = { { label: 'Can’t find your country? Select “Other” or contact an admin', value: '__INFO__', infoOption: true }, ], }, - render: (args) => { - const [{ value }, setArgs] = useArgs(); - - return ( - { - onChangeAction(val); - setArgs({ value: val }); - }} - /> - ); - }, + render: WithInfoOptionStory, }; -export const AutoSize: Story = { - args: { ...commonArgs, width: 'auto', minWidth: 20 }, - render: (args) => { - const [{ value }, setArgs] = useArgs(); +const AutoSizeStory: StoryFn = (args) => { + const [{ value }, setArgs] = useArgs(); + const comboboxId = useId(); - return ( + return ( + { action('onChange')(val); setArgs({ value: val }); }} /> - ); - }, + + ); +}; + +export const AutoSize: Story = { + args: { ...commonArgs, width: 'auto', minWidth: 20 }, + render: AutoSizeStory, }; const ManyOptionsStory: StoryFn = ({ numberOfOptions = 1e4, ...args }) => { const [dynamicArgs, setArgs] = useArgs(); - const [options, setOptions] = useState([]); + const comboboxId = useId(); useEffect(() => { setTimeout(async () => { @@ -123,15 +139,18 @@ const ManyOptionsStory: StoryFn = ({ numberOfOptions = 1e4, ... const { onChange, ...rest } = args; return ( - { - setArgs({ value: opts }); - onChangeAction(opts); - }} - /> + + { + setArgs({ value: opts }); + onChangeAction(opts); + }} + /> + ); }; @@ -146,8 +165,8 @@ export const ManyOptions: StoryObj = { const ManyOptionsGroupedStory: StoryFn = ({ numberOfOptions = 1e5, ...args }) => { const [dynamicArgs, setArgs] = useArgs(); - const [options, setOptions] = useState([]); + const comboboxId = useId(); useEffect(() => { setTimeout(async () => { @@ -157,15 +176,18 @@ const ManyOptionsGroupedStory: StoryFn = ({ numberOfOptions = 1 }, [numberOfOptions]); const { onChange, ...rest } = args; return ( - { - setArgs({ value: opts }); - onChangeAction(opts); - }} - /> + + { + setArgs({ value: opts }); + onChangeAction(opts); + }} + /> + ); }; @@ -183,6 +205,28 @@ function loadOptionsWithLabels(inputValue: string) { return fakeSearchAPI(`http://example.com/search?errorOnQuery=break&query=${inputValue}`); } +const AsyncOptionsWithLabelsStory: StoryFn = (args) => { + const [dynamicArgs, setArgs] = useArgs(); + const comboboxId = useId(); + + return ( + + { + onChangeAction(val); + setArgs({ value: val }); + }} + /> + + ); +}; + export const AsyncOptionsWithLabels: Story = { name: 'Async - options returns labels', args: { @@ -190,25 +234,7 @@ export const AsyncOptionsWithLabels: Story = { value: [{ label: 'Option 69', value: '69' }], placeholder: 'Select an option', }, - render: (args) => { - const [dynamicArgs, setArgs] = useArgs(); - - return ( - - { - onChangeAction(val); - setArgs({ value: val }); - }} - /> - - ); - }, + render: AsyncOptionsWithLabelsStory, }; function loadOptionsOnlyValues(inputValue: string) { @@ -218,6 +244,28 @@ function loadOptionsOnlyValues(inputValue: string) { ); } +const AsyncOptionsWithOnlyValuesStory: StoryFn = (args) => { + const [dynamicArgs, setArgs] = useArgs(); + const comboboxId = useId(); + + return ( + + { + onChangeAction(val); + setArgs({ value: val }); + }} + /> + + ); +}; + export const AsyncOptionsWithOnlyValues: Story = { name: 'Async - options returns only values', args: { @@ -225,23 +273,5 @@ export const AsyncOptionsWithOnlyValues: Story = { value: [{ value: 'Option 69' }], placeholder: 'Select an option', }, - render: (args) => { - const [dynamicArgs, setArgs] = useArgs(); - - return ( - - { - onChangeAction(val); - setArgs({ value: val }); - }} - /> - - ); - }, + render: AsyncOptionsWithOnlyValuesStory, }; diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index 572a9d865a0..2061087828d 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { MultiCombobox, MultiComboboxProps } from './MultiCombobox'; import { ComboboxOption } from './types'; +import { DEBOUNCE_TIME_MS } from './useOptions'; describe('MultiCombobox', () => { beforeAll(() => { @@ -330,7 +331,7 @@ describe('MultiCombobox', () => { await user.click(input); // Debounce - await act(async () => jest.advanceTimersByTime(200)); + await act(async () => jest.advanceTimersByTime(DEBOUNCE_TIME_MS)); expect(asyncOptions).toHaveBeenCalled(); }); @@ -380,10 +381,10 @@ describe('MultiCombobox', () => { await user.click(input); await user.keyboard('a'); - act(() => jest.advanceTimersByTime(200)); // Skip debounce + act(() => jest.advanceTimersByTime(DEBOUNCE_TIME_MS)); // Skip debounce await user.keyboard('b'); - act(() => jest.advanceTimersByTime(200)); // Skip debounce + act(() => jest.advanceTimersByTime(DEBOUNCE_TIME_MS)); // Skip debounce await user.keyboard('c'); act(() => jest.advanceTimersByTime(500)); // Resolve the second request, should be ignored @@ -422,7 +423,7 @@ describe('MultiCombobox', () => { act(() => jest.advanceTimersByTime(10)); await user.keyboard('c'); - act(() => jest.advanceTimersByTime(200)); + act(() => jest.advanceTimersByTime(DEBOUNCE_TIME_MS)); const item = await screen.findByRole('option', { name: 'Option 3' }); expect(item).toBeInTheDocument(); @@ -439,7 +440,7 @@ describe('MultiCombobox', () => { await user.click(input); // Debounce - await act(async () => jest.advanceTimersByTime(200)); + await act(async () => jest.advanceTimersByTime(DEBOUNCE_TIME_MS)); // Click on Option 1 to deselect it (it should already be selected via value prop) const item = await screen.findByRole('option', { name: 'Option 1' }); @@ -484,7 +485,7 @@ describe('MultiCombobox', () => { await user.click(input); // Wait for async options to load - await act(async () => jest.advanceTimersByTime(200)); + await act(async () => jest.advanceTimersByTime(DEBOUNCE_TIME_MS)); // Integration A should be selected (shown as pill) const pillRemoveButton = screen.getByRole('button', { name: 'Remove Integration A' }); @@ -500,6 +501,23 @@ describe('MultiCombobox', () => { // The pill should be removed expect(screen.queryByRole('button', { name: 'Remove Integration A' })).not.toBeInTheDocument(); }); + + it('shows loading message', async () => { + const loadingMessage = 'Loading options...'; + const asyncOptions = jest.fn(() => Promise.resolve(simpleAsyncOptions)); + render(); + + const input = screen.getByRole('combobox'); + await user.click(input); + + await act(async () => jest.advanceTimersByTime(0)); + + expect(await screen.findByText(loadingMessage)).toBeInTheDocument(); + + await act(async () => jest.advanceTimersByTime(DEBOUNCE_TIME_MS)); + + expect(screen.queryByText(loadingMessage)).not.toBeInTheDocument(); + }); }); }); diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 13dd02742e4..82759befffb 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -35,6 +35,11 @@ interface MultiComboboxBaseProps export type MultiComboboxProps = MultiComboboxBaseProps & AutoSizeConditionals; +/** + * The behavior of the MultiCombobox is similar to that of the Combobox, but it allows you to select multiple options. For all non-multi behaviors, see the Combobox documentation. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-multicombobox--docs + */ export const MultiCombobox = (props: MultiComboboxProps) => { const { placeholder, @@ -51,6 +56,8 @@ export const MultiCombobox = (props: MultiComboboxPro 'aria-labelledby': ariaLabelledBy, 'data-testid': dataTestId, portalContainer, + prefixIcon, + id, } = props; const styles = useStyles2(getComboboxStyles); @@ -146,17 +153,10 @@ export const MultiCombobox = (props: MultiComboboxPro }, }); - const { - getToggleButtonProps, - //getLabelProps, - isOpen, - highlightedIndex, - getMenuProps, - getInputProps, - getItemProps, - } = useCombobox({ + const { isOpen, highlightedIndex, getMenuProps, getInputProps, getItemProps } = useCombobox({ items: options, itemToString, + inputId: id, inputValue, selectedItem: null, stateReducer: (state, actionAndChanges) => { @@ -267,6 +267,13 @@ export const MultiCombobox = (props: MultiComboboxPro return (
+ {prefixIcon && ( + + + + + + )} {visibleItems.map((item, index) => ( (props: MultiComboboxPro })} /> -
+
{isClearable && selectedItems.length > 0 && ( (props: MultiComboboxPro > {isOpen && ( = const asyncNoop = () => Promise.resolve([]); +export const DEBOUNCE_TIME_MS = 200; + /** * Abstracts away sync/async options for combobox components. * It also filters options based on the user's input. @@ -49,7 +51,7 @@ export function useOptions(rawOptions: AsyncOptions React.ReactNode; } +/** + * A menu displaying additional options when it's not possible to show them at all times due to a space constraint. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-contextmenu--docs + */ export const ContextMenu = React.memo( ({ x, y, onClose, focusOnOpen = true, renderMenuItems, renderHeader }: ContextMenuProps) => { const menuRef = useRef(null); diff --git a/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx index a71a5979f58..98570eb2e15 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx @@ -3,6 +3,8 @@ import { t, Trans } from '@grafana/i18n'; import { InlineSwitch } from '../../components/Switch/Switch'; import { InlineField } from '../Forms/InlineField'; +import { Box } from '../Layout/Box/Box'; +import { Stack } from '../Layout/Stack/Stack'; export interface Props extends Pick, 'options' | 'onOptionsChange'> {} @@ -17,31 +19,36 @@ export function AlertingSettings({ options, onOptionsC

Alerting

-
-
-
- - - onOptionsChange({ - ...options, - jsonData: { ...options.jsonData, manageAlerts: event!.currentTarget.checked }, - }) - } - /> - -
-
-
+ + + + + + + onOptionsChange({ + ...options, + jsonData: { ...options.jsonData, manageAlerts: event!.currentTarget.checked }, + }) + } + /> + + + + + ); } diff --git a/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx index 49e2abf2a2a..d7d94b186c2 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx @@ -9,6 +9,8 @@ import { useStyles2 } from '../../themes/ThemeContext'; import { Button } from '../Button/Button'; import { FormField } from '../FormField/FormField'; import { Icon } from '../Icon/Icon'; +import { Box } from '../Layout/Box/Box'; +import { Stack } from '../Layout/Stack/Stack'; import { SecretFormField } from '../SecretFormField/SecretFormField'; export interface CustomHeader { @@ -199,12 +201,14 @@ export class CustomHeadersSettings extends PureComponent { const { dataSourceConfig } = this.props; return ( -
-
-
- Custom HTTP Headers -
-
+ + + +
+ Custom HTTP Headers +
+
+
{headers.map((header, i) => ( { ))}
{!dataSourceConfig.readOnly && ( -
- -
+ + + + + )} -
+ ); } } diff --git a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.mdx b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.mdx index 08671fcb108..f241ced1384 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.mdx +++ b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.mdx @@ -5,6 +5,8 @@ import { DataSourceHttpSettings } from './DataSourceHttpSettings'; # DataSourceHttpSettings +> **Deprecated!** Use components from `@grafana/plugin-ui` instead, according to the [migration guide](https://github.com/grafana/plugin-ui/blob/main/src/components/ConfigEditor/migrating-from-datasource-http-settings.md) + Component for displaying the configuration options for a data source plugin. ### When to use diff --git a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx index e57f7747494..65f6ff77259 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx @@ -72,6 +72,8 @@ const LABEL_WIDTH = 26; /** * @deprecated Use components from `@grafana/plugin-ui` instead, according to the [migration guide](https://github.com/grafana/plugin-ui/blob/main/src/components/ConfigEditor/migrating-from-datasource-http-settings.md). + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-datasourcehttpsettings--docs */ export const DataSourceHttpSettings = (props: HttpSettingsProps) => { const { diff --git a/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx index 6004fff49e6..d8708ddb470 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx @@ -3,6 +3,8 @@ import { t, Trans } from '@grafana/i18n'; import { InlineSwitch } from '../../components/Switch/Switch'; import { InlineField } from '../Forms/InlineField'; +import { Box } from '../Layout/Box/Box'; +import { Stack } from '../Layout/Stack/Stack'; export interface Props extends Pick, 'options' | 'onOptionsChange'> {} @@ -20,30 +22,32 @@ export function SecureSocksProxySettings({

Secure Socks Proxy

-
-
-
- - - onOptionsChange({ - ...options, - jsonData: { ...options.jsonData, enableSecureSocksProxy: event!.currentTarget.checked }, - }) - } - /> - -
-
-
+ + + + + + + onOptionsChange({ + ...options, + jsonData: { ...options.jsonData, enableSecureSocksProxy: event!.currentTarget.checked }, + }) + } + /> + + + + +
); } diff --git a/packages/grafana-ui/src/components/DataSourceSettings/TLSAuthSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/TLSAuthSettings.tsx index b5a1537edd8..fbd20c5c533 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/TLSAuthSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/TLSAuthSettings.tsx @@ -1,4 +1,3 @@ -import { css, cx } from '@emotion/css'; import * as React from 'react'; import { KeyValue } from '@grafana/data'; @@ -6,6 +5,8 @@ import { t, Trans } from '@grafana/i18n'; import { FormField } from '../FormField/FormField'; import { Icon } from '../Icon/Icon'; +import { Box } from '../Layout/Box/Box'; +import { Stack } from '../Layout/Stack/Stack'; import { Tooltip } from '../Tooltip/Tooltip'; import { CertificationKey } from './CertificationKey'; @@ -53,29 +54,24 @@ export const TLSAuthSettings = ({ dataSourceConfig, onChange }: HttpSettingsBase const privateKeyBeginsWith = '-----BEGIN RSA PRIVATE KEY-----'; return ( -
-
-
- TLS/SSL Auth Details -
- - - -
+ + + +
+ TLS/SSL Auth Details +
+ + + +
+
{dataSourceConfig.jsonData.tlsAuthWithCACert && ( -
- -
+ + + + + )}
-
+ ); }; diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx index 28c88547023..a69a6d9fb18 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx @@ -19,7 +19,12 @@ export interface DatePickerProps { maxDate?: Date; } -/** @public */ +/** + * A component with a calendar view for selecting a date. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/date-time-pickers-datepicker--docs + * @public + * */ export const DatePicker = memo((props) => { const styles = useStyles2(getStyles); const { isOpen, onClose } = props; diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx index 4d2d6533a31..0103a1d21a5 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx @@ -27,7 +27,12 @@ export interface DatePickerWithInputProps extends Omit( ({ value, minDate, maxDate, onChange, closeOnSelect, placeholder = 'Date', ...rest }, ref) => { const [open, setOpen] = useState(false); diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx index 1f4eb4da47d..a7024681d42 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx @@ -60,6 +60,11 @@ export interface Props { timeZone?: TimeZone; } +/** + * A component for selecting a date *and* time. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/date-time-pickers-datetimepicker--docs + */ export const DateTimePicker = ({ date, maxDate, diff --git a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx index 74a19dd6efd..e07345bf111 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx @@ -41,6 +41,7 @@ type InputState = { }; /** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/date-time-pickers-relativetimerangepicker--docs * @internal */ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx index 67d3af9496a..90666dad8b3 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx @@ -36,6 +36,11 @@ export interface TimeRangeInputProps { const noop = () => {}; +/** + * A variant of TimeRangePicker for use in forms. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/date-time-pickers-timerangeinput--docs + */ export const TimeRangeInput = ({ value, onChange, diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx index e99ada9c5d1..c7399e65a4f 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx @@ -187,14 +187,14 @@ describe('TimePickerTooltip', () => { it('renders time range without timezone if timezone is not passed in', () => { render(); - expect(screen.queryByText('United States, EDT')).not.toBeInTheDocument(); + expect(screen.queryByText(/United States, E[DS]T/)).not.toBeInTheDocument(); }); it('renders time range with browser timezone', () => { render(); expect(screen.getByText('Local browser time')).toBeInTheDocument(); - expect(screen.getByText('United States, EDT')).toBeInTheDocument(); // this was mocked at the beginning, in beforeAll block + expect(screen.getByText(/United States, E[DS]T/)).toBeInTheDocument(); // this was mocked at the beginning, in beforeAll block. matches either daylight savings time or standard time }); it('renders time range with specific timezone', () => { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx index 3157b6ca837..b26a6e4c36d 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx @@ -73,6 +73,9 @@ export interface State { isOpen: boolean; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/date-time-pickers-timerangepicker--docs + */ export function TimeRangePicker(props: TimeRangePickerProps) { const [isOpen, setOpen] = useState(false); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker.tsx index f41279cb983..464dcc9fb24 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker.tsx @@ -31,6 +31,9 @@ export interface Props { openMenuOnFocus?: boolean; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/date-time-pickers-timezonepicker--docs + */ export const TimeZonePicker = (props: Props) => { const { onChange, diff --git a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx index 0207dcb67a8..2fe37d06d3f 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx @@ -39,6 +39,9 @@ export function getWeekStart(override?: string): WeekStart { return 'monday'; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/date-time-pickers-weekstartpicker--docs + */ export const WeekStartPicker = (props: Props) => { const { onChange, width, autoFocus = false, onBlur, value, disabled = false, inputId } = props; const weekStarts: ComboboxOption[] = useMemo( diff --git a/packages/grafana-ui/src/components/Divider/Divider.tsx b/packages/grafana-ui/src/components/Divider/Divider.tsx index b9f5f580183..721ea8b58dc 100644 --- a/packages/grafana-ui/src/components/Divider/Divider.tsx +++ b/packages/grafana-ui/src/components/Divider/Divider.tsx @@ -9,6 +9,9 @@ interface DividerProps { spacing?: ThemeSpacingTokens; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-divider--docs + */ export const Divider = ({ direction = 'horizontal', spacing = 2 }: DividerProps) => { const styles = useStyles2(getStyles, spacing); diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx index b81a3784b72..b7ee0252bfc 100644 --- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx +++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx @@ -62,6 +62,11 @@ const drawerSizes = { lg: { width: '75vw', minWidth: 744 }, }; +/** + * Drawer is a slide in overlay that can be used to display additional information without hiding the main page content. It can be anchored to the left or right edge of the screen. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-drawer--docs + */ export function Drawer({ children, onClose, diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx index a2e23e78575..99f4db3b56b 100644 --- a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx @@ -25,6 +25,8 @@ export interface Props extends HTMLAttributes { /** * @deprecated Use Combobox or Dropdown instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-deprecated-buttonselect--docs */ const ButtonSelectComponent = (props: Props) => { const { className, options, value, onChange, narrow, variant, root, ...restProps } = props; diff --git a/packages/grafana-ui/src/components/Dropdown/Dropdown.tsx b/packages/grafana-ui/src/components/Dropdown/Dropdown.tsx index 77cdcc92519..b098665db7b 100644 --- a/packages/grafana-ui/src/components/Dropdown/Dropdown.tsx +++ b/packages/grafana-ui/src/components/Dropdown/Dropdown.tsx @@ -31,6 +31,11 @@ export interface Props { onVisibleChange?: (state: boolean) => void; } +/** + * Hook up a menu or other overlay to any trigger. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-dropdown--docs + */ export const Dropdown = React.memo(({ children, overlay, placement, offset, root, onVisibleChange }: Props) => { const [show, setShow] = useState(false); const transitionRef = useRef(null); diff --git a/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx b/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx index 2ddafbf865b..72f353b4ed7 100644 --- a/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx +++ b/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx @@ -8,7 +8,11 @@ export interface Props { children: JSX.Element | string; } -/** @deprecated Use instead */ +/** + * @deprecated Use `` instead. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-deprecated-emptysearchresult--docs + */ const EmptySearchResult = ({ children }: Props) => { const styles = useStyles2(getStyles); return
{children}
; diff --git a/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx b/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx index a7ee3774666..7e9c73fb0f1 100644 --- a/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx +++ b/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx @@ -38,6 +38,11 @@ interface Props { role?: AriaRole; } +/** + * The EmptyState component consists of a message and optionally an image, button, and additional information. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-emptystate--docs + */ export const EmptyState = ({ button, children, diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx index 63044d76d2f..818e919710c 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,6 +1,7 @@ -import { PureComponent, ReactNode, ComponentType, ErrorInfo } from 'react'; +import { PureComponent, ReactNode, ComponentType, ErrorInfo, memo } from 'react'; import { faro } from '@grafana/faro-web-sdk'; +import { t } from '@grafana/i18n'; import { Alert } from '../Alert/Alert'; @@ -33,6 +34,11 @@ interface State { errorInfo: ErrorInfo | null; } +/** + * A React component that catches errors in child components. Useful for logging or displaying a fallback UI in case of errors. More information about error boundaries is available at [React documentation website](https://reactjs.org/docs/error-boundaries.html). + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/utilities-errorboundary--docs + */ export class ErrorBoundary extends PureComponent { readonly state: State = { error: null, @@ -111,15 +117,9 @@ export interface ErrorBoundaryAlertProps { errorLogger?: (error: Error) => void; } -export class ErrorBoundaryAlert extends PureComponent { - static defaultProps: Partial = { - title: 'An unexpected error happened', - style: 'alertbox', - }; - - render() { - const { title, children, style, dependencies, errorLogger, boundaryName } = this.props; - +export const ErrorBoundaryAlert = memo( + ({ title, children, style = 'alertbox', dependencies, errorLogger, boundaryName }: ErrorBoundaryAlertProps) => { + const alertTitle = title ?? t('grafana-ui.error-boundary.title', 'An unexpected error happened'); return ( {({ error, errorInfo }) => { @@ -129,7 +129,7 @@ export class ErrorBoundaryAlert extends PureComponent { if (style === 'alertbox') { return ( - +
{error && error.toString()}
@@ -139,12 +139,14 @@ export class ErrorBoundaryAlert extends PureComponent { ); } - return ; + return ; }} ); } -} +); + +ErrorBoundaryAlert.displayName = 'ErrorBoundaryAlert'; /** * HOC for wrapping a component in an error boundary. diff --git a/packages/grafana-ui/src/components/FeatureBadge/FeatureBadge.tsx b/packages/grafana-ui/src/components/FeatureBadge/FeatureBadge.tsx index 6a67913e453..f1af47396f3 100644 --- a/packages/grafana-ui/src/components/FeatureBadge/FeatureBadge.tsx +++ b/packages/grafana-ui/src/components/FeatureBadge/FeatureBadge.tsx @@ -8,6 +8,11 @@ export interface FeatureBadgeProps { tooltip?: string; } +/** + * A component for displaying information about different release stages of features, in accordance with the guidelines provided at [Grafana's Release Life Cycle](https://grafana.com/docs/release-life-cycle). + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-featurebadge--docs + */ export const FeatureBadge = ({ featureState, tooltip }: FeatureBadgeProps) => { const display = getPanelStateBadgeDisplayModel(featureState); return ; diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx index 9ac5e775590..133022394f3 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx @@ -1,4 +1,7 @@ import { Meta, StoryFn } from '@storybook/react'; +import { useId } from 'react'; + +import { Field } from '../Forms/Field'; import { FileDropzone } from './FileDropzone'; import mdx from './FileDropzone.mdx'; @@ -10,12 +13,17 @@ const meta: Meta = { docs: { page: mdx, }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, }; -const Template: StoryFn = (args) => ; +const Template: StoryFn = (args) => { + const inputId = useId(); + return ( + + + + ); +}; export const Basic = Template.bind({}); diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx index 1d27de173a1..ad1fa0b8b49 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx @@ -47,6 +47,11 @@ export interface FileDropzoneProps { */ fileListRenderer?: (file: DropzoneFile, removeFile: (file: DropzoneFile) => void) => ReactNode; onFileRemove?: (file: DropzoneFile) => void; + /** + * Optional id attribute for the underlying input element + * Use to link a label to the input for accessibility + */ + id?: string; } export interface DropzoneFile { @@ -58,7 +63,20 @@ export interface DropzoneFile { retryUpload?: () => void; } -export function FileDropzone({ options, children, readAs, onLoad, fileListRenderer, onFileRemove }: FileDropzoneProps) { +/** + * A dropzone component to use for file uploads. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-filedropzone--docs + */ +export function FileDropzone({ + options, + children, + readAs, + onLoad, + fileListRenderer, + onFileRemove, + id, +}: FileDropzoneProps) { const [files, setFiles] = useState([]); const [fileErrors, setErrorMessages] = useState([]); @@ -218,7 +236,7 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender return (
- + {children ?? }
{fileErrors.length > 0 && renderErrorMessages(fileErrors)} diff --git a/packages/grafana-ui/src/components/FileDropzone/FileListItem.tsx b/packages/grafana-ui/src/components/FileDropzone/FileListItem.tsx index b13066621c6..e54d66b34b5 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileListItem.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileListItem.tsx @@ -17,6 +17,11 @@ export interface FileListItemProps { removeFile?: (file: DropzoneFile) => void; } +/** + * A FileListItem component used for the FileDropzone component to show uploaded files. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-filelistitem--docs + */ export function FileListItem({ file: customFile, removeFile }: FileListItemProps) { const styles = useStyles2(getStyles); const { file, progress, error, abortUpload, retryUpload } = customFile; diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx index 528d4a7e193..5ad7662ff56 100644 --- a/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx @@ -27,6 +27,11 @@ export interface Props { showFileName?: boolean; } +/** + * A button-styled input that triggers file upload popup. Button text and accepted file extensions can be customized via `label` and `accepted` props respectively. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-fileupload--docs + */ export const FileUpload = ({ onFileUpload, className, diff --git a/packages/grafana-ui/src/components/FilterPill/FilterPill.tsx b/packages/grafana-ui/src/components/FilterPill/FilterPill.tsx index 48140e3940d..6fdbaa2e76d 100644 --- a/packages/grafana-ui/src/components/FilterPill/FilterPill.tsx +++ b/packages/grafana-ui/src/components/FilterPill/FilterPill.tsx @@ -15,6 +15,11 @@ export interface FilterPillProps { icon?: IconName; } +/** + * A component used for quick toggling on/off filters. Mostly used in inline form components and transformation/query editors. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-filterpill--docs + */ export const FilterPill = ({ label, selected, onClick, icon = 'check' }: FilterPillProps) => { const styles = useStyles2(getStyles); const clearButton = useStyles2(clearButtonStyles); diff --git a/packages/grafana-ui/src/components/FormField/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx index 4d9bca9ded8..c4663db842e 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.tsx @@ -24,6 +24,8 @@ export interface Props extends InputHTMLAttributes { * * For inline fields, use {@link InlineField}, {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-inlinefield--basic See Storybook}. * @deprecated Please use the {@link Field} component, {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-field--simple See Storybook}. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-deprecated-formfield--docs */ export const FormField = ({ label, diff --git a/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.tsx b/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.tsx index 6c9d6dd5334..f2e0527939a 100644 --- a/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.tsx +++ b/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.tsx @@ -18,6 +18,11 @@ function fontSizeReductionFactor(fontSize: number) { return 0.6; } +/** + * Used to display a value, which also supports prefix and suffix. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-formattedvaluedisplay--docs + */ export const FormattedValueDisplay = ({ value, className, style, ...htmlProps }: Props) => { const hasPrefix = (value.prefix ?? '').length > 0; const hasSuffix = (value.suffix ?? '').length > 0; diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.tsx index 173f821e287..c20b0892e21 100644 --- a/packages/grafana-ui/src/components/Forms/Checkbox.tsx +++ b/packages/grafana-ui/src/components/Forms/Checkbox.tsx @@ -24,6 +24,9 @@ export interface CheckboxProps extends Omit, 'value' invalid?: boolean; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-checkbox--docs + */ export const Checkbox = React.forwardRef( ( { label, description, value, htmlValue, onChange, disabled, className, indeterminate, invalid, ...inputProps }, diff --git a/packages/grafana-ui/src/components/Forms/Field.tsx b/packages/grafana-ui/src/components/Forms/Field.tsx index 1c80a2caf81..6c5884e04c9 100644 --- a/packages/grafana-ui/src/components/Forms/Field.tsx +++ b/packages/grafana-ui/src/components/Forms/Field.tsx @@ -43,6 +43,11 @@ export interface FieldProps extends HTMLAttributes { noMargin?: boolean; } +/** + * Field is the basic component for rendering form elements together with labels and description. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-field--docs + */ export const Field = React.forwardRef( ( { diff --git a/packages/grafana-ui/src/components/Forms/FieldArray.tsx b/packages/grafana-ui/src/components/Forms/FieldArray.tsx index fc0cda3b7ff..5c21767ab84 100644 --- a/packages/grafana-ui/src/components/Forms/FieldArray.tsx +++ b/packages/grafana-ui/src/components/Forms/FieldArray.tsx @@ -9,6 +9,8 @@ export interface FieldArrayProps extends UseFieldArrayProps { /** * @deprecated use the `useFieldArray` hook from react-hook-form instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-fieldarray--docs */ export const FieldArray: FC = ({ name, control, children, ...rest }) => { const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({ diff --git a/packages/grafana-ui/src/components/Forms/FieldSet.tsx b/packages/grafana-ui/src/components/Forms/FieldSet.tsx index 421ba3a5ad3..4f42cb96f8e 100644 --- a/packages/grafana-ui/src/components/Forms/FieldSet.tsx +++ b/packages/grafana-ui/src/components/Forms/FieldSet.tsx @@ -14,6 +14,11 @@ export interface Props extends Omit, 'label'> { label?: React.ReactNode; } +/** + * Component used to group form elements inside a form, equivalent to HTML's [fieldset](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset) tag. Accepts optional label, which, if provided, is used as a text for the set's legend. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-fieldset--docs + */ export const FieldSet = ({ label, children, className, ...rest }: Props) => { const styles = useStyles2(getStyles); diff --git a/packages/grafana-ui/src/components/Forms/FieldValidationMessage.tsx b/packages/grafana-ui/src/components/Forms/FieldValidationMessage.tsx index 78f1aeab79e..3a3fd5b078b 100644 --- a/packages/grafana-ui/src/components/Forms/FieldValidationMessage.tsx +++ b/packages/grafana-ui/src/components/Forms/FieldValidationMessage.tsx @@ -12,6 +12,11 @@ export interface FieldValidationMessageProps { horizontal?: boolean; } +/** + * Component for displaying a validation error message under an element. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-fieldvalidationmessage--docs + */ export const FieldValidationMessage = ({ children, horizontal, diff --git a/packages/grafana-ui/src/components/Forms/Form.tsx b/packages/grafana-ui/src/components/Forms/Form.tsx index 2eb45c69a0e..c251780be96 100644 --- a/packages/grafana-ui/src/components/Forms/Form.tsx +++ b/packages/grafana-ui/src/components/Forms/Form.tsx @@ -18,6 +18,8 @@ interface FormProps extends Omit({ defaultValues, diff --git a/packages/grafana-ui/src/components/Forms/InlineField.mdx b/packages/grafana-ui/src/components/Forms/InlineField.mdx index 1b13b8d3811..b08be513645 100644 --- a/packages/grafana-ui/src/components/Forms/InlineField.mdx +++ b/packages/grafana-ui/src/components/Forms/InlineField.mdx @@ -3,7 +3,7 @@ import { InlineField } from './InlineField'; # InlineField -A basic component for rendering form elements, like `Input`, `Select`, `Checkbox`, etc, inline together with `InlineLabel`. If the child element has `id` specified, the label's `htmlFor` attribute, pointing to the id, will be added. +A basic component for rendering form elements, like `Input`, `Checkbox`, `Combobox`, etc, inline together with `InlineLabel`. If the child element has `id` specified, the label's `htmlFor` attribute, pointing to the id, will be added. The width of the `InlineLabel` can be modified via `labelWidth` prop, which is a multiple of 8px. For example, an `InlineField` with `labelWidth={20}` will have a label 160px wide. diff --git a/packages/grafana-ui/src/components/Forms/InlineField.tsx b/packages/grafana-ui/src/components/Forms/InlineField.tsx index c63d87b585b..a2e7bd4f9e7 100644 --- a/packages/grafana-ui/src/components/Forms/InlineField.tsx +++ b/packages/grafana-ui/src/components/Forms/InlineField.tsx @@ -29,6 +29,11 @@ export interface Props extends Omit, 'css'> { children: ReactNode | ReactNode[]; } +/** + * Used to align multiple InlineField components in one row. The row will wrap if the width of the children exceeds its own. Equivalent to the div with gf-form-inline class name. Multiple InlineFieldRows vertically stack on each other. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-inlinefieldrow--docs + */ export const InlineFieldRow = ({ children, className, ...htmlProps }: Props) => { const styles = useStyles2(getStyles); return ( diff --git a/packages/grafana-ui/src/components/Forms/InlineLabel.mdx b/packages/grafana-ui/src/components/Forms/InlineLabel.mdx index 9a451f07922..be5b5766a9d 100644 --- a/packages/grafana-ui/src/components/Forms/InlineLabel.mdx +++ b/packages/grafana-ui/src/components/Forms/InlineLabel.mdx @@ -3,7 +3,7 @@ import { InlineLabel } from './InlineLabel'; # InlineLabel -A horizontal variant of `Label`, primarily used in query editors. Can be combined with form components that expect a label, eg. `Input`, `Select`, `Checkbox`. +A horizontal variant of `Label`, primarily used in query editors. Can be combined with form components that expect a label, eg. `Input`, `Checkbox`, `Combobox`. If you need to add additional explanation, use the tooltip prop, which will render an info icon with tooltip inside the label. For query editor readability, the label text should be as short as possible (4 words or fewer). diff --git a/packages/grafana-ui/src/components/Forms/InlineLabel.tsx b/packages/grafana-ui/src/components/Forms/InlineLabel.tsx index fef27352c7f..63b7b7ccb46 100644 --- a/packages/grafana-ui/src/components/Forms/InlineLabel.tsx +++ b/packages/grafana-ui/src/components/Forms/InlineLabel.tsx @@ -25,6 +25,11 @@ export interface Props extends Omit category?: React.ReactNode[]; } +/** + * The label component can be used to label form inputs with a heading/"Option name" and a description. To automatically have the right arrangement of this component with a form input, use the `Field` component. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-label--docs + */ export const Label = ({ children, description, className, category, ...labelProps }: LabelProps) => { const styles = useStyles2(getLabelStyles); const categories = category?.map((c, i) => { diff --git a/packages/grafana-ui/src/components/Forms/Legend.tsx b/packages/grafana-ui/src/components/Forms/Legend.tsx index 7b48c149b7b..90af8e1229e 100644 --- a/packages/grafana-ui/src/components/Forms/Legend.tsx +++ b/packages/grafana-ui/src/components/Forms/Legend.tsx @@ -21,6 +21,11 @@ export const getLegendStyles = (theme: GrafanaTheme2) => { }; }; +/** + * Legend should be used to add a caption to a group of related form elements that have been grouped toegheter into a `FieldSet`. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-legend--docs + */ export const Legend = ({ children, className, ...legendProps }: LabelProps) => { const styles = useStyles2(getLegendStyles); diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx index b47ff9913d3..ebef41cbed8 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx @@ -24,6 +24,11 @@ export interface RadioButtonGroupProps { invalid?: boolean; } +/** + * RadioButtonGroup is used to select a single value from multiple mutually exclusive options. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-radiobuttongroup--docs + */ export function RadioButtonGroup({ options, value, diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.tsx index a6366c64756..6127d143438 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonList.tsx @@ -22,6 +22,11 @@ export interface RadioButtonListProps { className?: string; } +/** + * RadioButtonList is used to select a single value from multiple mutually exclusive options usually in a vertical manner. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-radiobuttonlist--docs + */ export function RadioButtonList({ name, id, diff --git a/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index ed987489f14..fa4e1fac6d3 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -42,6 +42,11 @@ const getIconStyles = (theme: GrafanaTheme2) => { }; }; +/** + * Grafana's icon wrapper component. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/iconography-icon--docs + */ export const Icon = React.memo( React.forwardRef( ({ size = 'md', type = 'default', name, className, style, title = '', ...rest }, ref) => { diff --git a/packages/grafana-ui/src/components/IconButton/IconButton.tsx b/packages/grafana-ui/src/components/IconButton/IconButton.tsx index bbc4519f71f..f2d44eb2fa1 100644 --- a/packages/grafana-ui/src/components/IconButton/IconButton.tsx +++ b/packages/grafana-ui/src/components/IconButton/IconButton.tsx @@ -41,8 +41,18 @@ interface BasePropsWithAriaLabel extends BaseProps { ['aria-label']: string; } -export type Props = BasePropsWithTooltip | BasePropsWithAriaLabel; +interface BasePropsWithAriaLabelledBy extends BaseProps { + /** Reference to an element id that labels the button. No tooltip will be set in this case. */ + ['aria-labelledby']: string; +} +export type Props = BasePropsWithTooltip | BasePropsWithAriaLabel | BasePropsWithAriaLabelledBy; + +/** + * This component looks just like an icon but behaves like a button. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-iconbutton--docs + */ export const IconButton = React.forwardRef((props, ref) => { const { size = 'md', variant = 'secondary' } = props; let limitedIconSize: LimitedIconSize; diff --git a/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx b/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx index cb445321333..bf3e0e7f69c 100644 --- a/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx +++ b/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx @@ -23,7 +23,11 @@ export interface InfoBoxProps extends Omit, onDismiss?: () => void; } -/** @deprecated use Alert with severity info */ +/** + * @deprecated use Alert with severity info. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-deprecated-infobox--docs + * */ export const InfoBox = React.memo( React.forwardRef( ({ title, className, children, branded, url, urlTitle, onDismiss, severity = 'info', ...otherProps }, ref) => { diff --git a/packages/grafana-ui/src/components/InfoTooltip/InfoTooltip.tsx b/packages/grafana-ui/src/components/InfoTooltip/InfoTooltip.tsx index 6f189837c43..9af5cadaa74 100644 --- a/packages/grafana-ui/src/components/InfoTooltip/InfoTooltip.tsx +++ b/packages/grafana-ui/src/components/InfoTooltip/InfoTooltip.tsx @@ -6,7 +6,11 @@ interface InfoTooltipProps extends Omit { children: PopoverContent; } -/** @deprecated Use instead */ +/** + * @deprecated Use instead. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-deprecated-infotooltip--docs + */ export const InfoTooltip = ({ children, ...restProps }: InfoTooltipProps) => { return ; }; diff --git a/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx b/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx index 178b0032b1e..de79c028d9f 100644 --- a/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx +++ b/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx @@ -24,6 +24,11 @@ export interface InlineToastProps { alternativePlacement?: Side; } +/** + * Used to indicate temporal status near fields/components, such as a *Saved* indicator next to a field, or a little *Copied!* indicator above a button. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-inlinetoast--docs + */ export function InlineToast({ referenceElement, children, suffixIcon, placement }: InlineToastProps) { const styles = useStyles2(getStyles); const theme = useTheme2(); diff --git a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx index 2cf17612d99..2e533cf065f 100644 --- a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx +++ b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx @@ -20,6 +20,11 @@ export interface Props extends InputProps { defaultValue?: string | number | readonly string[]; } +/** + * You can use it or regular text input. When used, AutoSizeInput resizes itself to the current content. For an array of data or tree-structured data, consider using `Combobox` or `Cascader` respectively. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-autosizeinput--docs + */ export const AutoSizeInput = React.forwardRef((props, ref) => { const { defaultValue = '', diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 62aca888d03..4da6c9c22c7 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -34,6 +34,11 @@ interface StyleDeps { width?: number; } +/** + * Used for regular text input. For an array of data or tree-structured data, consider using `Combobox` or `Cascader` respectively. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-input--docs + */ export const Input = forwardRef((props, ref) => { const { className, diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx index 4c0faebccf5..a046f24b89c 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx @@ -172,7 +172,13 @@ interface WithoutExpandableRow extends BaseProps = WithExpandableRow | WithoutExpandableRow; -/** @alpha */ +/** + * The InteractiveTable is used to display and select data efficiently. It allows for the display and modification of detailed information. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-interactivetable--docs + * + * @alpha + */ export function InteractiveTable({ className, columns, diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.tsx b/packages/grafana-ui/src/components/Layout/Box/Box.tsx index c012eb9d5f3..a7d10e4c746 100644 --- a/packages/grafana-ui/src/components/Layout/Box/Box.tsx +++ b/packages/grafana-ui/src/components/Layout/Box/Box.tsx @@ -70,6 +70,11 @@ export interface BoxProps extends FlexProps, SizeProps, Omit; } +/** + * The Box Component is the most basic layout component. It can be used to build more complex components and layouts with properties that use our design tokens instead of using CSS. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-box--docs + */ export const Box = forwardRef>((props, ref) => { const { children, diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx index 1573f881a04..ffba1eb88de 100644 --- a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx @@ -34,6 +34,11 @@ interface PropsWithMinColumnWidth extends GridPropsBase { /** 'columns' and 'minColumnWidth' are mutually exclusive */ type GridProps = PropsWithColumns | PropsWithMinColumnWidth; +/** + * The Grid component is a layout component that allows you to create a grid of columns and rows to organize content and elements. It is a wrapper around the [CSS Grid](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout) specification. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-grid--docs + */ export const Grid = forwardRef((props, ref) => { const { alignItems, children, gap, rowGap, columnGap, columns, minColumnWidth, ...rest } = props; const styles = useStyles2(getGridStyles, gap, rowGap, columnGap, columns, minColumnWidth, alignItems); diff --git a/packages/grafana-ui/src/components/Layout/Layout.tsx b/packages/grafana-ui/src/components/Layout/Layout.tsx index a2f34d44d06..71fe0803b83 100644 --- a/packages/grafana-ui/src/components/Layout/Layout.tsx +++ b/packages/grafana-ui/src/components/Layout/Layout.tsx @@ -33,6 +33,8 @@ export interface ContainerProps { /** * @deprecated use Stack component instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-deprecated-groups--docs */ export const Layout = ({ children, diff --git a/packages/grafana-ui/src/components/Layout/Space.mdx b/packages/grafana-ui/src/components/Layout/Space.mdx index 47bc0cb8e54..dd5182a8cea 100644 --- a/packages/grafana-ui/src/components/Layout/Space.mdx +++ b/packages/grafana-ui/src/components/Layout/Space.mdx @@ -5,7 +5,7 @@ import { Space } from './Space'; # Space -The `Space` component is a component used to add space between elements. Horizontal space is added using the `h` prop, while vertical space is added using the `v` prop. When adding horizontal space between inline or inline-block elements, the `layout` props should be set to `inline`, otherwise the `block` value of the prop can be used. +The `Space` component is a component used to add space between elements. Horizontal space is added using the `h` prop, while vertical space is added using the `v` prop. When adding horizontal space between inline or inline-block elements, the `layout` prop should be set to `inline`, otherwise the `block` value of the prop can be used. ### Usage diff --git a/packages/grafana-ui/src/components/Layout/Space.tsx b/packages/grafana-ui/src/components/Layout/Space.tsx index 2df48501ea4..8a81bcb1eed 100644 --- a/packages/grafana-ui/src/components/Layout/Space.tsx +++ b/packages/grafana-ui/src/components/Layout/Space.tsx @@ -19,6 +19,11 @@ export interface SpaceProps { layout?: 'block' | 'inline'; } +/** + * The Space component is a component used to add space between elements. Horizontal space is added using the `h` prop, while vertical space is added using the `v` prop. When adding horizontal space between inline or inline-block elements, the `layout` prop should be set to `"inline"`, otherwise the `"block"` value of the prop can be used. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-space--docs + */ export const Space = ({ v = 0, h = 0, layout }: SpaceProps) => { return ; }; diff --git a/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx b/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx index e54cc724f2d..b3a4984b110 100644 --- a/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx +++ b/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx @@ -21,7 +21,8 @@ interface StackProps extends FlexProps, SizeProps, Omit((props, ref) => { const { diff --git a/packages/grafana-ui/src/components/Link/TextLink.tsx b/packages/grafana-ui/src/components/Link/TextLink.tsx index d46a1ba3bc3..1f7ec1e16de 100644 --- a/packages/grafana-ui/src/components/Link/TextLink.tsx +++ b/packages/grafana-ui/src/components/Link/TextLink.tsx @@ -43,6 +43,11 @@ const svgSizes: { bodySmall: 'xs', }; +/** + * The TextLink component renders an anchor tag `` that takes users to another page, external or internal to Grafana. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/foundations-textlink--docs + */ export const TextLink = forwardRef( ( { href, color = 'link', external = false, inline = true, variant = 'body', weight, icon, children, ...rest }, diff --git a/packages/grafana-ui/src/components/List/List.tsx b/packages/grafana-ui/src/components/List/List.tsx index a58bb3c5d4e..d4b86807a7b 100644 --- a/packages/grafana-ui/src/components/List/List.tsx +++ b/packages/grafana-ui/src/components/List/List.tsx @@ -2,7 +2,11 @@ import { PureComponent } from 'react'; import { ListProps, AbstractList } from './AbstractList'; -/** @deprecated Use ul/li/arr.map directly instead */ +/** + * @deprecated Use ul/li/arr.map directly instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-deprecated-list--docs + */ // no point converting, this is deprecated // eslint-disable-next-line react-prefer-function-component/react-prefer-function-component export class List extends PureComponent> { diff --git a/packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx b/packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx index 213f39b4243..1ac038fe75f 100644 --- a/packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx +++ b/packages/grafana-ui/src/components/LoadingBar/LoadingBar.tsx @@ -18,6 +18,11 @@ const MAX_DURATION_MS = 4000; const DEFAULT_ANIMATION_DELAY = 300; const MAX_TRANSLATE_X = (100 / BAR_WIDTH) * 100; +/** + * The LoadingBar is used as a simple loading slider animation in the top of its container. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-loadingbar--docs + */ export function LoadingBar({ width, delay = DEFAULT_ANIMATION_DELAY, ariaLabel = 'Loading bar' }: LoadingBarProps) { const durationMs = Math.min(Math.max(Math.round(width * MILLISECONDS_PER_PIXEL), MIN_DURATION_MS), MAX_DURATION_MS); const styles = useStyles2(getStyles, delay, durationMs); diff --git a/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx b/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx index fb3ec626437..9f50ed9d072 100644 --- a/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx +++ b/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx @@ -15,6 +15,9 @@ export interface LoadingPlaceholderProps extends HTMLAttributes } /** + * Loading indicator with a text. Used to alert a user to wait for an activity to complete. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-loadingplaceholder--docs * @public */ export const LoadingPlaceholder = ({ text, className, ...rest }: LoadingPlaceholderProps) => { diff --git a/packages/grafana-ui/src/components/Menu/Menu.tsx b/packages/grafana-ui/src/components/Menu/Menu.tsx index f77a26bb0e7..a1439aa5356 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.tsx @@ -22,6 +22,9 @@ export interface MenuProps extends React.HTMLAttributes { onKeyDown?: React.KeyboardEventHandler; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-menu--docs + */ const MenuComp = React.forwardRef( ({ header, children, ariaLabel, onOpen, onClose, onKeyDown, ...otherProps }, forwardedRef) => { const styles = useStyles2(getStyles); diff --git a/packages/grafana-ui/src/components/Modal/Modal.tsx b/packages/grafana-ui/src/components/Modal/Modal.tsx index 9937fe93f9f..fc00bc9e562 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.tsx @@ -48,6 +48,9 @@ interface WithCustomTitleProps extends BaseProps { export type Props = WithStringTitleProps | WithCustomTitleProps; +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-modal--docs + */ export function Modal(props: PropsWithChildren) { const { title, diff --git a/packages/grafana-ui/src/components/Modal/ModalsContext.tsx b/packages/grafana-ui/src/components/Modal/ModalsContext.tsx index 4a540716fee..734e3fe620b 100644 --- a/packages/grafana-ui/src/components/Modal/ModalsContext.tsx +++ b/packages/grafana-ui/src/components/Modal/ModalsContext.tsx @@ -17,10 +17,6 @@ export const ModalsContext = React.createContext({ interface ModalsProviderProps { children: React.ReactNode; - /** Set default component to render as modal. Useful when rendering modals from Angular */ - component?: React.ComponentType | null; - /** Set default component props. Useful when rendering modals from Angular */ - props?: any; } /** @@ -31,8 +27,8 @@ export class ModalsProvider extends Component { } } +/** + * Monaco Code editor. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-codeeditor--docs + */ export const CodeEditor = withTheme2(UnthemedCodeEditor); const getStyles = (theme: GrafanaTheme2) => { diff --git a/packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx b/packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx index 296d79212f6..93e59296166 100644 --- a/packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx +++ b/packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx @@ -34,7 +34,11 @@ export interface Props { forceShowLeftItems?: boolean; } -/** @deprecated Use Page instead */ +/** + * @deprecated Use Page instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/navigation-deprecated-pagetoolbar--docs + */ export const PageToolbar = memo( ({ title, diff --git a/packages/grafana-ui/src/components/Pagination/Pagination.tsx b/packages/grafana-ui/src/components/Pagination/Pagination.tsx index 70c447e059c..c0c25d4186b 100644 --- a/packages/grafana-ui/src/components/Pagination/Pagination.tsx +++ b/packages/grafana-ui/src/components/Pagination/Pagination.tsx @@ -21,6 +21,11 @@ export interface Props { className?: string; } +/** + * Component used for rendering a page selector below paginated content. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/navigation-pagination--docs + */ export const Pagination = ({ currentPage, numberOfPages, diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx index e312691bc4c..1b6342d035a 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx @@ -32,8 +32,6 @@ const meta: Meta = { docs: { page: mdx, }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, }; @@ -46,7 +44,7 @@ function getContentStyle(): CSSProperties { }; } -function renderPanel(name: string, overrides?: Partial) { +function renderPanel(content: string, overrides?: Partial) { const props: PanelChromeProps = { width: PANEL_WIDTH, height: PANEL_HEIGHT, @@ -60,7 +58,7 @@ function renderPanel(name: string, overrides?: Partial) { return ( {(innerWidth: number, innerHeight: number) => { - return
{name}
; + return
{content}
; }}
); @@ -133,70 +131,74 @@ export const Examples = () => {
- {renderPanel('Has statusMessage', { - title: 'Default title', + {renderPanel('Content', { + title: 'Panel with statusMessage', statusMessage: 'Error text', statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No padding, has statusMessage', { + {renderPanel('Content', { padding: 'none', - title: 'Default title', + title: 'Panel with statusMessage and no padding', statusMessage: 'Error text', statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No title, loadingState is Error, no statusMessage', { + {renderPanel('Content', { loadingState: LoadingState.Error, + title: 'No title, loadingState is Error, no statusMessage', })} - {renderPanel('loadingState is Streaming', { - title: 'Default title', + {renderPanel('Content', { + title: 'loadingState is Streaming', loadingState: LoadingState.Streaming, })} - {renderPanel('loadingState is Loading', { - title: 'Default title', + {renderPanel('Content', { + title: 'loadingState is Loading', loadingState: LoadingState.Loading, })} {renderPanel('Default panel: no non-required props')} - {renderPanel('No padding', { + {renderPanel('Content', { padding: 'none', + title: 'No padding', })} - {renderPanel('Very long title', { + {renderPanel('Content', { title: 'Very long title that should get ellipsis when there is no more space', })} - {renderPanel('No title, streaming loadingState', { + {renderPanel('Content', { + title: 'No title, streaming loadingState', loadingState: LoadingState.Streaming, })} - {renderPanel('Error status, menu', { - title: 'Default title', + {renderPanel('Content', { + title: 'Error status, menu', menu, statusMessage: 'Error text', statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No padding; has statusMessage, menu', { + {renderPanel('Content', { padding: 'none', - title: 'Default title', + title: 'No padding; has statusMessage, menu', menu, statusMessage: 'Error text', statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No title, loadingState is Error, no statusMessage, menu', { + {renderPanel('Content', { + title: 'No title, loadingState is Error, no statusMessage, menu', menu, loadingState: LoadingState.Error, })} - {renderPanel('loadingState is Streaming, menu', { - title: 'Default title', + {renderPanel('Content', { + title: 'loadingState is Streaming, menu', menu, loadingState: LoadingState.Streaming, })} - {renderPanel('loadingState is Loading, menu', { - title: 'Default title', + {renderPanel('Content', { + title: 'loadingState is Loading, menu', menu, loadingState: LoadingState.Loading, })} - {renderPanel('No padding, deprecated loading indicator', { + {renderPanel('Content', { padding: 'none', - title: 'Default title', + title: 'No padding, deprecated loading indicator', leftItems: [ { />, ], })} - {renderPanel('Display mode = transparent', { - title: 'Default title', + {renderPanel('Content', { + title: 'Display mode = transparent', displayMode: 'transparent', menu, })} - {renderPanel('Actions with button no menu', { + {renderPanel('Content', { title: 'Actions with button no menu', actions: ( ), })} - {renderPanel('Panel with two actions', { - title: 'I have two buttons', + {renderPanel('Content', { + title: 'Panel with two actions', actions: [ ); @@ -59,7 +65,6 @@ const getStyles = (theme: GrafanaTheme2) => { const item = css({ color: `${theme.colors.text.secondary}`, label: 'panel-header-item', - cursor: 'auto', border: 'none', borderRadius: `${theme.shape.radius.default}`, padding: `${theme.spacing(0, 1)}`, @@ -84,5 +89,6 @@ const getStyles = (theme: GrafanaTheme2) => { return { item, linkItem: cx(item, css({ cursor: 'pointer' })), + buttonItem: cx(item, css({ cursor: 'pointer' })), }; }; diff --git a/packages/grafana-ui/src/components/PanelContainer/PanelContainer.tsx b/packages/grafana-ui/src/components/PanelContainer/PanelContainer.tsx index 5279c363675..fd68335b2a7 100644 --- a/packages/grafana-ui/src/components/PanelContainer/PanelContainer.tsx +++ b/packages/grafana-ui/src/components/PanelContainer/PanelContainer.tsx @@ -8,7 +8,11 @@ import { useStyles2 } from '../../themes/ThemeContext'; type Props = DetailedHTMLProps, HTMLDivElement>; // TODO: Reimplement this with Box -/** @deprecated Use Box instead */ +/** + * @deprecated Use Box instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/layout-deprecated-panelcontainer--docs + */ export const PanelContainer = ({ children, className, ...props }: Props) => { const styles = useStyles2(getStyles); return ( diff --git a/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx b/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx index 698e61c466b..9935f2323d1 100644 --- a/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx +++ b/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx @@ -22,6 +22,8 @@ export interface PluginSignatureBadgeProps extends HTMLAttributes { plugins: Array>; runOnChangeDebounced: Function; @@ -238,6 +232,16 @@ export class UnThemedQueryField extends PureComponent { diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 128c56b1cb6..3dcc25e8875 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -72,6 +72,9 @@ export type RadialGradientMode = 'none' | 'auto'; export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'none'; export type RadialShape = 'circle' | 'gauge'; +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-radialgauge--docs + */ export function RadialGauge(props: RadialGaugeProps) { const { width = 256, diff --git a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx index d99958e88d1..363222fcad6 100644 --- a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx +++ b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx @@ -29,6 +29,11 @@ export interface Props { isOnCanvas?: boolean; } +/** + * This component is used on dashboards to refresh visualizations. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-refreshpicker--docs + */ export class RefreshPicker extends PureComponent { static offOption = { label: 'Off', diff --git a/packages/grafana-ui/src/components/RenderUserContentAsHTML/RenderUserContentAsHTML.tsx b/packages/grafana-ui/src/components/RenderUserContentAsHTML/RenderUserContentAsHTML.tsx index d5ec22c2440..3fb806bc162 100644 --- a/packages/grafana-ui/src/components/RenderUserContentAsHTML/RenderUserContentAsHTML.tsx +++ b/packages/grafana-ui/src/components/RenderUserContentAsHTML/RenderUserContentAsHTML.tsx @@ -9,6 +9,11 @@ export interface RenderUserContentAsHTMLProps content: string; } +/** + * Abstraction layer component for sanitizing and rendering an html content. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/utilities-renderusercontentashtml--docs + */ export function RenderUserContentAsHTML({ component, content, diff --git a/packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.tsx b/packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.tsx index af7907b9562..60da6f97a8d 100644 --- a/packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.tsx +++ b/packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.tsx @@ -17,6 +17,11 @@ interface Props extends Omit>( ( { diff --git a/packages/grafana-ui/src/components/SecretFormField/SecretFormField.tsx b/packages/grafana-ui/src/components/SecretFormField/SecretFormField.tsx index 4bfaac7c603..238d3adf3ea 100644 --- a/packages/grafana-ui/src/components/SecretFormField/SecretFormField.tsx +++ b/packages/grafana-ui/src/components/SecretFormField/SecretFormField.tsx @@ -32,6 +32,8 @@ export interface Props extends Omit, 'onRe * to the user (like datasource passwords). * * @deprecated Please use the {@link SecretInput} component with a {@link Field} instead, {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-secretinput--basic as seen in Storybook} + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/forms-deprecated-secretformfield--docs */ export const SecretFormField = ({ label = 'Password', diff --git a/packages/grafana-ui/src/components/SecretInput/SecretInput.tsx b/packages/grafana-ui/src/components/SecretInput/SecretInput.tsx index b7cc7b41a72..23286c5a393 100644 --- a/packages/grafana-ui/src/components/SecretInput/SecretInput.tsx +++ b/packages/grafana-ui/src/components/SecretInput/SecretInput.tsx @@ -14,6 +14,11 @@ export type Props = React.ComponentProps & { export const CONFIGURED_TEXT = 'configured'; export const RESET_BUTTON_TEXT = 'Reset'; +/** + * Used for secret/password input. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-secretinput--docs + */ export const SecretInput = ({ isConfigured, onReset, ...props }: Props) => ( {!isConfigured && } diff --git a/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.tsx b/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.tsx index b7196732660..a10ad157120 100644 --- a/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.tsx +++ b/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.tsx @@ -32,6 +32,8 @@ const getStyles = (theme: GrafanaTheme2) => { /** * Text area that does not disclose an already configured value but lets the user reset the current value and enter a new one. * Typically useful for asymmetric cryptography keys. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-secrettextarea--docs */ export const SecretTextArea = ({ isConfigured, onReset, ...props }: Props) => { const styles = useStyles2(getStyles); diff --git a/packages/grafana-ui/src/components/Segment/Segment.tsx b/packages/grafana-ui/src/components/Segment/Segment.tsx index 3abec60b774..b39399965c4 100644 --- a/packages/grafana-ui/src/components/Segment/Segment.tsx +++ b/packages/grafana-ui/src/components/Segment/Segment.tsx @@ -20,6 +20,9 @@ export interface SegmentSyncProps extends SegmentProps, Omit({ options, value, diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx index 804b1bbe92f..8a3caf6e73e 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx @@ -29,6 +29,9 @@ export interface SegmentAsyncProps extends SegmentProps, Omit({ value, onChange, diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.tsx index 22e9411297e..bcb46c93f04 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentInput.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentInput.tsx @@ -20,6 +20,9 @@ export interface SegmentInputProps const FONT_SIZE = 14; +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-segmentinput--docs + */ export function SegmentInput({ value: initialValue, onChange, diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx index f543fa5dab5..7aaa338ae1c 100644 --- a/packages/grafana-ui/src/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -10,11 +10,20 @@ import { VirtualizedSelectAsyncProps, } from './types'; -/** @deprecated Use Combobox component instead */ +/** + * @deprecated Use Combobox component instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-deprecated-select--docs + */ export function Select(props: SelectCommonProps & Rest) { return ; } +/** + * @deprecated Use Combobox component instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-deprecated-select--docs + */ export function MultiSelect(props: MultiSelectCommonProps & Rest) { // @ts-ignore return ; @@ -25,17 +34,29 @@ export interface AsyncSelectProps extends Omit, 'options value?: T | SelectableValue | null; } -/** @deprecated Use Combobox component instead */ +/** + * @deprecated Use Combobox component instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-deprecated-select--docs + */ export function AsyncSelect(props: AsyncSelectProps & Rest) { return ; } -/** @deprecated Use Combobox component instead - it's virtualised by default! */ +/** + * @deprecated Use Combobox component instead - it's virtualised by default! + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-deprecated-select--docs + */ export function VirtualizedSelect(props: VirtualizedSelectProps & Rest) { return ; } -/** @deprecated Use Combobox component instead - it's virtualised by default! */ +/** + * @deprecated Use Combobox component instead - it's virtualised by default! + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-deprecated-select--docs + */ export function AsyncVirtualizedSelect(props: VirtualizedSelectAsyncProps & Rest) { return ; } @@ -45,6 +66,11 @@ interface AsyncMultiSelectProps extends Omit, 'opti value?: Array>; } +/** + * @deprecated Use Combobox component instead + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-deprecated-select--docs + */ export function AsyncMultiSelect(props: AsyncMultiSelectProps & Rest) { // @ts-ignore return ; diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 0eb6a1fefeb..8609c307c25 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -195,9 +195,9 @@ export function SelectBase({ const creatableProps: ComponentProps>> = {}; let asyncSelectProps: any = {}; - let selectedValue; + let selectedValue: any; if (isMulti && loadOptions) { - selectedValue = value as any; + selectedValue = value; } else { // If option is passed as a plain value (value property from SelectableValue property) // we are selecting the corresponding value from the options diff --git a/packages/grafana-ui/src/components/Select/SelectMenu.tsx b/packages/grafana-ui/src/components/Select/SelectMenu.tsx index bb12cf4dcfb..bfb8b17a1c1 100644 --- a/packages/grafana-ui/src/components/Select/SelectMenu.tsx +++ b/packages/grafana-ui/src/components/Select/SelectMenu.tsx @@ -245,8 +245,8 @@ const ToggleAllOption = ({ innerProps: {}, children: ( <> - Selected - {`(${selectedCount ?? 0})`} + Selected + {` (${selectedCount ?? 0})`} ), })} diff --git a/packages/grafana-ui/src/components/Select/resetSelectStyles.ts b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts index 067cdaadf76..90ac3877c2a 100644 --- a/packages/grafana-ui/src/components/Select/resetSelectStyles.ts +++ b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts @@ -51,7 +51,7 @@ export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | stri return useMemo(() => { return { ...resetSelectStyles(theme), - menuPortal: (base: any) => { + menuPortal: (base: CSSObjectWithLabel) => { // Would like to correct top position when menu is placed bottom, but have props are not sent to this style function. // Only state is. https://github.com/JedWatson/react-select/blob/master/packages/react-select/src/components/Menu.tsx#L605 return { @@ -60,7 +60,7 @@ export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | stri }; }, //These are required for the menu positioning to function - menu: ({ top, bottom, position }: any) => { + menu: ({ top, bottom, position }: CSSObjectWithLabel) => { return { top, bottom, @@ -73,7 +73,7 @@ export function useCustomSelectStyles(theme: GrafanaTheme2, width: number | stri width: width ? theme.spacing(width) : '100%', display: width === 'auto' ? 'inline-flex' : 'flex', }), - option: (provided: any, state: any) => ({ + option: (provided: CSSObjectWithLabel, state: any) => ({ ...provided, opacity: state.isDisabled ? 0.5 : 1, }), diff --git a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts index cf2dc4fffb0..b9aa005074c 100644 --- a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts +++ b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts @@ -197,7 +197,7 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel + +## Design decisions + +The slider state deals with two sources of truth: the value that's passed in, and the text input value. The text input value needs to be synchronized with external value changes, but it can also have its own temporarily invalid values. diff --git a/packages/grafana-ui/src/components/Slider/Slider.test.tsx b/packages/grafana-ui/src/components/Slider/Slider.test.tsx index e24ccd7e751..4028ab1b750 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.test.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.test.tsx @@ -4,6 +4,8 @@ import userEvent from '@testing-library/user-event'; import { Slider } from './Slider'; import { SliderProps } from './types'; +import '@testing-library/jest-dom'; + const sliderProps: SliderProps = { min: 10, max: 20, @@ -17,6 +19,37 @@ describe('Slider', () => { user = userEvent.setup(); }); + it('respects min/max bounds after decimal input blur', async () => { + render(); + + const sliderInput = screen.getByRole('textbox'); + + // Above max + await user.clear(sliderInput); + await user.type(sliderInput, '15.2'); + await user.click(document.body); + expect(sliderInput).toHaveValue('10'); // max enforced + + // Below min + await user.clear(sliderInput); + await user.type(sliderInput, '-2.7'); + await user.click(document.body); + expect(sliderInput).toHaveValue('0'); // min enforced + }); + + it('updates slider value correctly when decimal input is typed', async () => { + render(); + + const slider = screen.getByRole('slider'); + const sliderInput = screen.getByRole('textbox'); + + await user.clear(sliderInput); + await user.type(sliderInput, '7.3'); + await user.click(document.body); + + expect(slider).toHaveAttribute('aria-valuenow', '7.4'); + }); + it('renders without error', () => { expect(() => render()).not.toThrow(); }); @@ -33,6 +66,18 @@ describe('Slider', () => { expect(sliderInput).toHaveValue('10'); }); + it('hides the slider input if showInput is false', () => { + render(); + + const slider = screen.getByRole('slider'); + const sliderInput = screen.queryByRole('textbox'); + + expect(slider).toHaveAttribute('aria-valuemin', '10'); + expect(slider).toHaveAttribute('aria-valuemax', '20'); + expect(slider).toHaveAttribute('aria-valuenow', '10'); + expect(sliderInput).not.toBeInTheDocument(); + }); + it('renders correct contents with a value', () => { render(); @@ -62,6 +107,66 @@ describe('Slider', () => { expect(sliderInput).toHaveValue('50'); }); + it('allows decimal numbers in input', async () => { + render(); + const sliderInput = screen.getByRole('textbox'); + const slider = screen.getByRole('slider'); + + await user.clear(sliderInput); + await user.type(sliderInput, '3.5'); + + expect(sliderInput).toHaveValue('3.5'); + + // numeric value clamped + expect(slider).toHaveAttribute('aria-valuenow', '4'); + }); + + it('number parsing ignores non-numeric characters typed in the text input', async () => { + render(); + const sliderInput = screen.getByRole('textbox'); + const slider = screen.getByRole('slider'); + + await user.clear(sliderInput); + + // the characters other than numbers and the first `-` and `.` are stripped as you type + await user.type(sliderInput, 'ab-cd1ef.gh.1'); + + expect(sliderInput).toHaveValue('ab-cd1ef.gh.1'); + expect(slider).toHaveAttribute('aria-valuenow', '-1.1'); + }); + + it('number parsing allows but ignores non-numeric characters typed in the text input when step and min are integers', async () => { + render(); + const sliderInput = screen.getByRole('textbox'); + const slider = screen.getByRole('slider'); + + await user.clear(sliderInput); + + // the characters other than numbers and the first `-` and `.` are stripped as you type + await user.type(sliderInput, 'ab-cd1ef1gh6ij.5kl'); + + expect(sliderInput).toHaveValue('ab-cd1ef1gh6ij.5kl'); + + // value is clamped from 116 to 115 + expect(slider).toHaveAttribute('aria-valuenow', '-115'); + }); + + // this is because it's a bit confusing when the value is zeroed out and you click the input that you + // can't type "-" immediately and it's an easy case to handle + it('allows you to type "-" when the value is "0"', async () => { + render(); + const sliderInput = screen.getByRole('textbox'); + const slider = screen.getByRole('slider'); + + await user.clear(sliderInput); + + // the zero is stripped + await user.type(sliderInput, '0-1'); + + expect(sliderInput).toHaveValue('-1'); + expect(slider).toHaveAttribute('aria-valuenow', '-1'); + }); + it('sets value to the closest available one after blur if input value is outside of range', async () => { render(); @@ -86,4 +191,43 @@ describe('Slider', () => { expect(sliderInput).toHaveValue('10'); expect(slider).toHaveAttribute('aria-valuenow', '10'); }); + + // the rest of the tests are uncontrolled already, don't need to separately test that + it('can be a controlled input', async () => { + const mockOnChange = jest.fn(); + const props: SliderProps = { + ...sliderProps, + onChange: mockOnChange, + min: -10, + max: 100, + }; + + const { rerender } = render(); + const slider = screen.getByRole('slider'); + const sliderInput = screen.getByRole('textbox'); + + await user.type(sliderInput, '-1'); + // click outside the input field to blur + await user.click(document.body); + + expect(slider).toHaveAttribute('aria-valuenow', '-1'); + expect(sliderInput).toHaveValue('-1'); + + // Called once while typing "-1" (initial is "0", then "-", which is NaN and doesn't call + // onChange) and once more on blur + expect(mockOnChange).toHaveBeenCalledTimes(2); + expect(mockOnChange).toHaveBeenCalledWith(-1); + + rerender(); + + rerender(); + + // onChange should not be called when slider is re-rendered with a new value + // this check ensure the state synchronization is working properly, since accidentally + // causing onChange calls is a easy failure mode if that code is modified + expect(mockOnChange).toHaveBeenCalledTimes(2); + + expect(slider).toHaveAttribute('aria-valuenow', '45'); + expect(sliderInput).toHaveValue('45'); + }); }); diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx index e8c377070bf..9458b500532 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.tsx @@ -1,7 +1,8 @@ import { cx } from '@emotion/css'; import { Global } from '@emotion/react'; import SliderComponent from 'rc-slider'; -import { useState, useCallback, ChangeEvent, FocusEvent } from 'react'; +import { useState, useCallback, ChangeEvent, FocusEvent, useEffect } from 'react'; +import { usePrevious } from 'react-use'; import { t } from '@grafana/i18n'; @@ -11,8 +12,67 @@ import { Input } from '../Input/Input'; import { getStyles } from './styles'; import { SliderProps } from './types'; +function stripAndParseNumber(raw: string): number { + const str = raw.replace(/^0+/, ''); + let decimal = false; + let numericBody = ''; + for (let i = 0; i < str.length; i += 1) { + const char = str.charAt(i); + + // take digits + if (/\d/.test(char)) { + numericBody += char; + } + // take the first period + if (char === '.' && !decimal) { + decimal = true; + numericBody += '.'; + } + // take only a leading negative sign + if (char === '-' && numericBody.length === 0) { + numericBody = '-'; + } + + // anything else is thrown away + } + const value = Number(numericBody); + return value; +} + +// gets rid of pesky things like 1.20000000000000002 and such, since this needs to be printed +// nicely for people. +function roundFloatingPointError(n: number) { + return parseFloat(n.toPrecision(12)); +} + +function clampToAllowedValue(min: number, max: number, step: number, n: number): number { + // default to min + if (Number.isNaN(n)) { + return min; + } + + // clamp to max and min + if (n > max) { + return max; + } + if (n < min) { + return min; + } + + // ensure the value is exactly one of the allowed steps + // find the closest step + const closestStep = roundFloatingPointError(Math.round((n - min) / step) * step + min); + + // clamp the closest found step to min/max + // this should never be needed unless the step isn't divisible by max-min, but it's a + // quick and easy check to include. + return Math.min(max, Math.max(min, closestStep)); +} + /** * @public + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-slider--docs */ export const Slider = ({ min, @@ -21,88 +81,94 @@ export const Slider = ({ onAfterChange, orientation = 'horizontal', reverse, - step, + step = 1, value, ariaLabelForHandle, marks, included, inputId, + showInput = true, }: SliderProps) => { const isHorizontal = orientation === 'horizontal'; const styles = useStyles2(getStyles, isHorizontal, Boolean(marks)); const SliderWithTooltip = SliderComponent; - const [sliderValue, setSliderValue] = useState(value ?? min); + + const [inputValue, setInputValue] = useState((value ?? min).toString()); + const numericValue = clampToAllowedValue(min, max, step, stripAndParseNumber(inputValue)); + + // State synchronization. This is a hack since we have to maintain our own source of truth for the text input + const previousValue = usePrevious(value); + const externalValueChanged = value !== previousValue && value !== numericValue; + useEffect(() => { + if (externalValueChanged && value !== undefined) { + // This only causes a re-render if the value is actually different, which should + // only happen if the value is externally changed + setInputValue(String(value)); + } + }, [externalValueChanged, value]); + const dragHandleAriaLabel = ariaLabelForHandle ?? t('grafana-ui.slider.drag-handle-aria-label', 'Use arrow keys to change the value'); const onSliderChange = useCallback( (v: number | number[]) => { - const value = typeof v === 'number' ? v : v[0]; - - setSliderValue(value); - onChange?.(value); + const num = typeof v === 'number' ? v : v[0]; + setInputValue(num.toString()); + onChange?.(num); }, - [setSliderValue, onChange] - ); - - const onSliderInputChange = useCallback( - (e: ChangeEvent) => { - let v = +e.target.value; - - if (Number.isNaN(v)) { - v = 0; - } - - setSliderValue(v); - - if (onChange) { - onChange(v); - } - - if (onAfterChange) { - onAfterChange(v); - } - }, - [onChange, onAfterChange] - ); - - // Check for min/max on input blur so user is able to enter - // custom values that might seem above/below min/max on first keystroke - const onSliderInputBlur = useCallback( - (e: FocusEvent) => { - const v = +e.target.value; - - if (v > max) { - setSliderValue(max); - } else if (v < min) { - setSliderValue(min); - } - }, - [max, min] + [onChange] ); const handleChangeComplete = useCallback( (v: number | number[]) => { - const value = typeof v === 'number' ? v : v[0]; - onAfterChange?.(value); + const num = typeof v === 'number' ? v : v[0]; + onAfterChange?.(num); }, [onAfterChange] ); + const onTextInputChange = useCallback( + (e: ChangeEvent) => { + const raw = e.target.value; + + // Update the raw input string to show what user typed, except the special case of `0-`, which + // should result in just `-` as a user convenience. + setInputValue(raw === '0-' ? '-' : raw); + + // Parse and validate the number + const parsed = stripAndParseNumber(raw); + if (onChange && !Number.isNaN(parsed)) { + // Clamp the output value + onChange(clampToAllowedValue(min, max, step, parsed)); + } + }, + [onChange, min, max, step] + ); + + const onTextInputBlur = useCallback( + (e: FocusEvent) => { + const parsed = clampToAllowedValue(min, max, step, stripAndParseNumber(e.target.value)); + + // Update both numeric and string values with the clamped result + setInputValue(parsed.toString()); + onChange?.(parsed); + onAfterChange?.(parsed); + }, + [min, max, step, onChange, onAfterChange] + ); + const sliderInputClassNames = !isHorizontal ? [styles.sliderInputVertical] : []; const sliderInputFieldClassNames = !isHorizontal ? [styles.sliderInputFieldVertical] : []; return (
- {/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */}
- + {showInput && ( + + )}
); diff --git a/packages/grafana-ui/src/components/Slider/types.ts b/packages/grafana-ui/src/components/Slider/types.ts index 812a8bed7d7..880bfd2f712 100644 --- a/packages/grafana-ui/src/components/Slider/types.ts +++ b/packages/grafana-ui/src/components/Slider/types.ts @@ -14,6 +14,8 @@ interface CommonSliderProps { marks?: SliderMarks; /** If the value is true, it means a continuous value interval, otherwise, it is a independent value. */ included?: boolean; + /** Controls visibility of the input field. Defaults to true. */ + showInput?: boolean; } export interface SliderProps extends CommonSliderProps { value?: number; diff --git a/packages/grafana-ui/src/components/Spinner/Spinner.tsx b/packages/grafana-ui/src/components/Spinner/Spinner.tsx index 78905e29eec..af1c3b60aca 100644 --- a/packages/grafana-ui/src/components/Spinner/Spinner.tsx +++ b/packages/grafana-ui/src/components/Spinner/Spinner.tsx @@ -29,6 +29,10 @@ interface PropsWithDeprecatedSize extends Omit { /** * @public + * + * Spinner is `fa-spinner` icon animated. It is used to alert a user to wait for an activity to complete. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-spinner--docs */ export const Spinner = ({ className, diff --git a/packages/grafana-ui/src/components/Splitter/useSplitter.ts b/packages/grafana-ui/src/components/Splitter/useSplitter.ts index b5343ae80aa..18fb95a532e 100644 --- a/packages/grafana-ui/src/components/Splitter/useSplitter.ts +++ b/packages/grafana-ui/src/components/Splitter/useSplitter.ts @@ -49,6 +49,11 @@ const propsForDirection = { }, } as const; +/** + * The splitter creates two resizable panes, either horizontally or vertically. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/utilities-usesplitter--docs + */ export function useSplitter(options: UseSplitterOptions) { const { direction, diff --git a/packages/grafana-ui/src/components/Switch/Switch.tsx b/packages/grafana-ui/src/components/Switch/Switch.tsx index 0a8654b1425..d2b63a2d76a 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.tsx @@ -14,6 +14,11 @@ export interface Props extends Omit, 'value'> { invalid?: boolean; } +/** + * Switch is a representation of an on-off state – like a light switch. So you can use Switch to toggle binary states. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-switch--docs + */ export const Switch = forwardRef( ({ value, checked, onChange, id, label, disabled, invalid = false, ...inputProps }, ref) => { if (checked) { diff --git a/packages/grafana-ui/src/components/Table/TableCellInspector.tsx b/packages/grafana-ui/src/components/Table/TableCellInspector.tsx index ed7f9111773..10169328ba4 100644 --- a/packages/grafana-ui/src/components/Table/TableCellInspector.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellInspector.tsx @@ -1,8 +1,10 @@ -import { isString } from 'lodash'; +import { css } from '@emotion/css'; import { useState } from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; +import { useStyles2 } from '../../themes/ThemeContext'; import { ClipboardButton } from '../ClipboardButton/ClipboardButton'; import { Drawer } from '../Drawer/Drawer'; import { Stack } from '../Layout/Stack/Stack'; @@ -17,34 +19,15 @@ export enum TableCellInspectorMode { interface TableCellInspectorProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any; + value: string; onDismiss: () => void; mode: TableCellInspectorMode; } export function TableCellInspector({ value, onDismiss, mode }: TableCellInspectorProps) { - let displayValue = value; const [currentMode, setMode] = useState(mode); - - if (isString(value)) { - const trimmedValue = value.trim(); - // Exclude numeric strings like '123' from being displayed in code/JSON mode - if (trimmedValue[0] === '{' || trimmedValue[0] === '[' || mode === 'code') { - try { - value = JSON.parse(value); - displayValue = JSON.stringify(value, null, ' '); - } catch (error: any) { - // Display helpful error to help folks diagnose json errors - console.log( - 'Failed to parse JSON in Table cell inspector (this will cause JSON to not print nicely): ', - error.message - ); - } - } - } else { - displayValue = JSON.stringify(value); - } - let text = displayValue; + const text = value.trim(); + const styles = useStyles2(getStyles); const tabs = [ { @@ -81,15 +64,23 @@ export function TableCellInspector({ value, onDismiss, mode }: TableCellInspecto height={500} language="json" showLineNumbers={true} - showMiniMap={(text && text.length) > 100} + showMiniMap={(text ? text.length : 0) > 100} value={text} readOnly={true} wordWrap={true} /> ) : ( -
{text}
+
{text}
)}
); } + +// TODO: should we have different empty styles? +const getStyles = (theme: GrafanaTheme2) => ({ + textContainer: css({ + color: theme.colors.text.secondary, + minHeight: 42, + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx index f8b20c8f312..f7a46e13add 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx @@ -14,6 +14,7 @@ describe('PillCell', () => { type: FieldType.string, values: values, config: {}, + display: (value: unknown) => ({ text: String(value), color: '#FF780A', numeric: NaN }), }); const ser = new XMLSerializer(); @@ -165,6 +166,29 @@ describe('PillCell', () => { ` ); }); + + it('custom display text', () => { + const mockField = fieldWithValues(['value1,value2,value3']); + const field = { + ...mockField, + display: (value: unknown) => ({ + text: `${value} lbs`, + color: '#FF780A', + numeric: 0, + }), + } satisfies Field; + + expectHTML( + render( + + ), + ` + value1 lbs + value2 lbs + value3 lbs + ` + ); + }); }); describe('Color by value mappings', () => { diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx index d304df96da3..1aecedbd961 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx @@ -8,6 +8,7 @@ import { getColorByStringHash, FALLBACK_COLOR, fieldColorModeRegistry, + formattedValueToString, } from '@grafana/data'; import { FieldColorModeId } from '@grafana/schema'; @@ -20,10 +21,11 @@ export function PillCell({ rowIdx, field, theme, getTextColorForBackground }: Pi const pillValues = inferPills(value); return pillValues.length > 0 ? pillValues.map((pill, index) => { - const bgColor = getPillColor(pill, field, theme); + const renderedValue = formattedValueToString(field.display!(pill)); + const bgColor = getPillColor(renderedValue, field, theme); const textColor = getTextColorForBackground(bgColor); return { - value: String(pill), + value: renderedValue, key: `${pill}-${index}`, bgColor, color: textColor, diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx index 08f8b8b6cd1..453fdb237b0 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx @@ -1,15 +1,7 @@ import { css } from '@emotion/css'; import * as React from 'react'; -import { - FieldType, - FieldConfig, - getMinMaxAndDelta, - FieldSparkline, - isDataFrame, - Field, - isDataFrameWithValue, -} from '@grafana/data'; +import { FieldConfig, getMinMaxAndDelta, Field, isDataFrameWithValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { BarAlignment, @@ -25,8 +17,9 @@ import { import { measureText } from '../../../../utils/measureText'; import { FormattedValueDisplay } from '../../../FormattedValueDisplay/FormattedValueDisplay'; import { Sparkline } from '../../../Sparkline/Sparkline'; +import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink'; import { SparklineCellProps, TableCellStyles } from '../types'; -import { getAlignmentFactor, getCellOptions } from '../utils'; +import { getAlignmentFactor, getCellOptions, prepareSparklineValue } from '../utils'; export const defaultSparklineCellConfig: TableSparklineCellOptions = { type: TableCellDisplayMode.Sparkline, @@ -43,10 +36,14 @@ export const defaultSparklineCellConfig: TableSparklineCellOptions = { export const SparklineCell = (props: SparklineCellProps) => { const { field, value, theme, timeRange, rowIdx, width } = props; - const sparkline = getSparkline(value, field); + const sparkline = prepareSparklineValue(value, field); if (!sparkline) { - return <>{field.config.noValue || t('grafana-ui.table.sparkline.no-data', 'no data')}; + return ( + + {field.config.noValue || t('grafana-ui.table.sparkline.no-data', 'no data')} + + ); } // Get the step from the first two values to null-fill the x-axis based on timerange @@ -95,37 +92,13 @@ export const SparklineCell = (props: SparklineCellProps) => { } return ( - <> + {valueElement} - + ); }; -function getSparkline(value: unknown, field: Field): FieldSparkline | undefined { - if (Array.isArray(value)) { - return { - y: { - name: `${field.name}-sparkline`, - type: FieldType.number, - values: value, - config: {}, - }, - }; - } - - if (isDataFrame(value)) { - const timeField = value.fields.find((x) => x.type === FieldType.time); - const numberField = value.fields.find((x) => x.type === FieldType.number); - - if (timeField && numberField) { - return { x: timeField, y: numberField }; - } - } - - return; -} - function getTableSparklineCellOptions(field: Field): TableSparklineCellOptions { let options = getCellOptions(field); if (options.type === TableCellDisplayMode.Auto) { @@ -139,8 +112,12 @@ function getTableSparklineCellOptions(field: Field): TableSparklineCellOptions { export const getStyles: TableCellStyles = (theme, { textAlign }) => css({ - width: '100%', - gap: theme.spacing(1), - justifyContent: 'space-between', - ...(textAlign === 'right' && { flexDirection: 'row-reverse' }), + '&, & > a': { + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: theme.spacing(1), + ...(textAlign === 'right' && { flexDirection: 'row-reverse' }), + }, }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 17c54fde979..a3772785d63 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -94,7 +94,6 @@ import { predicateByName, shouldTextOverflow, shouldTextWrap, - withDataLinksActionsTooltip, getSummaryCellTextAlign, parseStyleJson, IS_SAFARI_26, @@ -406,7 +405,6 @@ export function TableNG(props: TableNGProps) { const result: FromFieldsResult = { columns: [], cellRootRenderers: {}, - colsWithTooltip: {}, }; let lastRowIdx = -1; @@ -439,7 +437,7 @@ export function TableNG(props: TableNGProps) { // attach JSONCell custom display function to JSONView cell type if (cellType === TableCellDisplayMode.JSONView || field.type === FieldType.other) { - field.display = displayJsonValue; + field.display = displayJsonValue(field); } // For some cells, "aligning" the cell will mean aligning the inline contents of the cell with @@ -464,7 +462,6 @@ export function TableNG(props: TableNGProps) { const shouldOverflow = !IS_SAFARI_26 && rowHeight !== 'auto' && (shouldTextOverflow(field) || Boolean(maxRowHeight)); const textWrap = rowHeight === 'auto' || shouldTextWrap(field); - const withTooltip = withDataLinksActionsTooltip(field, cellType); const canBeColorized = canFieldBeColorized(cellType, applyToRowBgFn); const cellStyleOptions: TableCellStyleOptions = { textAlign, @@ -473,8 +470,6 @@ export function TableNG(props: TableNGProps) { maxHeight: maxRowHeight, }; - result.colsWithTooltip[displayName] = withTooltip; - const defaultCellStyles = getDefaultCellStyles(theme, cellStyleOptions); const cellSpecificStyles = getCellSpecificStyles(cellType, field, theme, cellStyleOptions); const linkStyles = getLinkStyles(theme, canBeColorized); @@ -563,7 +558,6 @@ export function TableNG(props: TableNGProps) { { + const { columns, cellRootRenderers } = useMemo(() => { const result = fromFields(visibleFields, widths); // if nested frames are present, augment the columns to include the nested table expander column. @@ -806,7 +800,6 @@ export function TableNG(props: TableNGProps) { const field = columns[column.idx].field; if ( - colsWithTooltip[getDisplayName(field)] && target instanceof HTMLElement && // this walks up the tree to find either a faux link wrapper or the cell root // it then only proceeds if we matched the faux link wrapper diff --git a/packages/grafana-ui/src/components/Table/TableNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/Table/TableNG/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000000..922b7514309 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/__snapshots__/utils.test.ts.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TableNG utils buildInspectValue should handle NaN 1`] = ` +[ + "NaN", + "text", +] +`; + +exports[`TableNG utils buildInspectValue should handle array 1`] = ` +[ + "[ + "foo", + "bar", + "baz" +]", + "code", +] +`; + +exports[`TableNG utils buildInspectValue should handle boolean 1`] = ` +[ + "true", + "text", +] +`; + +exports[`TableNG utils buildInspectValue should handle geo 1`] = ` +[ + "POINT(0 -0.0006656516255105771)", + "code", +] +`; + +exports[`TableNG utils buildInspectValue should handle null 1`] = ` +[ + "", + "text", +] +`; + +exports[`TableNG utils buildInspectValue should handle null w/ JSON 1`] = ` +[ + "null", + "code", +] +`; + +exports[`TableNG utils buildInspectValue should handle numbers 1`] = ` +[ + "0", + "text", +] +`; + +exports[`TableNG utils buildInspectValue should handle object 1`] = ` +[ + "{ + "foo": true, + "b": "baz" +}", + "code", +] +`; + +exports[`TableNG utils buildInspectValue should handle sparkline (no x) 1`] = ` +[ + "[ + 2, + 4, + 6, + 8, + 10 +]", + "code", +] +`; + +exports[`TableNG utils buildInspectValue should handle sparkline 1`] = ` +[ + "[ + [0, 10], + [1000, 20], + [2000, 30] +]", + "code", +] +`; + +exports[`TableNG utils buildInspectValue should handle string 1`] = ` +[ + "foo", + "text", +] +`; + +exports[`TableNG utils buildInspectValue should handle string w/ JSON 1`] = ` +[ + ""baz"", + "code", +] +`; + +exports[`TableNG utils buildInspectValue should handle undefined 1`] = ` +[ + "", + "text", +] +`; diff --git a/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx index b43728dd1e8..7758bf8fd20 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx @@ -34,4 +34,5 @@ export const MaybeWrapWithLink = memo(({ field, rowIdx, children }: MaybeWrapWit // raw value return children; }); + MaybeWrapWithLink.displayName = 'MaybeWrapWithLink'; diff --git a/packages/grafana-ui/src/components/Table/TableNG/components/TableCellActions.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/TableCellActions.tsx index de9333718d6..caf71634557 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/components/TableCellActions.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/components/TableCellActions.tsx @@ -1,27 +1,13 @@ -import WKT from 'ol/format/WKT'; -import Geometry from 'ol/geom/Geometry'; import { memo } from 'react'; -import { FieldType } from '@grafana/data'; import { t } from '@grafana/i18n'; import { IconButton } from '../../../IconButton/IconButton'; -import { TableCellInspectorMode } from '../../TableCellInspector'; -import { TableCellDisplayMode } from '../../types'; import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellActionsProps } from '../types'; +import { buildInspectValue } from '../utils'; export const TableCellActions = memo( - ({ - field, - value, - cellOptions, - displayName, - setInspectCell, - onCellFilterAdded, - className, - cellInspect, - showFilters, - }: TableCellActionsProps) => ( + ({ field, value, setInspectCell, onCellFilterAdded, className, cellInspect, showFilters }: TableCellActionsProps) => ( // stopping propagation to prevent clicks within the actions menu from triggering the cell click events // for things like the data links tooltip. // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions @@ -31,24 +17,8 @@ export const TableCellActions = memo( name="eye" aria-label={t('grafana-ui.table.cell-inspect-tooltip', 'Inspect value')} onClick={() => { - let inspectValue = value; - let mode = TableCellInspectorMode.text; - - if (field.type === FieldType.geo && value instanceof Geometry) { - inspectValue = new WKT().writeGeometry(value, { - featureProjection: 'EPSG:3857', - dataProjection: 'EPSG:4326', - }); - mode = TableCellInspectorMode.code; - } - if (cellOptions.type === TableCellDisplayMode.JSONView) { - mode = TableCellInspectorMode.code; - } - - setInspectCell({ - value: String(inspectValue ?? ''), - mode, - }); + const [inspectValue, mode] = buildInspectValue(value, field); + setInspectCell({ value: inspectValue, mode }); }} /> )} diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index 376fe895ba2..b828dfddbb6 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -167,12 +167,12 @@ export type InspectCellProps = { rowIdx?: number; value: string; mode?: TableCellInspectorMode.code | TableCellInspectorMode.text; + preformatted?: boolean; }; export interface TableCellActionsProps { field: Field; value: TableCellValue; - cellOptions: TableCellOptions; displayName: string; cellInspect: boolean; showFilters: boolean; @@ -308,7 +308,6 @@ export type CellRootRenderer = (key: React.Key, props: CellRendererProps; - colsWithTooltip: Record; } export interface FooterFieldState extends FieldState { diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts index 9109375ae17..02527df8540 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts @@ -1,3 +1,4 @@ +import { Point } from 'ol/geom'; import { SortColumn } from 'react-data-grid'; import { @@ -48,6 +49,9 @@ import { predicateByName, parseStyleJson, calculateFooterHeight, + displayJsonValue, + prepareSparklineValue, + buildInspectValue, } from './utils'; describe('TableNG utils', () => { @@ -1355,10 +1359,43 @@ describe('TableNG utils', () => { }); describe('displayJsonValue', () => { - it.todo('should parse and then stringify string values'); - it.todo('should not throw for non-serializable string values'); - it.todo('should stringify non-string values'); - it.todo('should not throw for non-serializable non-string values'); + let field: Field; + beforeEach(() => { + field = { + name: 'test', + type: FieldType.string, + config: {}, + state: { displayName: 'Test Display Name' }, + values: [], + display: (val: unknown) => ({ text: String(val), numeric: NaN }), + }; + }); + + it('should parse and then stringify string values', () => { + expect(displayJsonValue(field)('{"valid": "json"}').text).toBe('{\n "valid": "json"\n}'); + }); + + it('should not throw for non-serializable string values', () => { + expect(displayJsonValue(field)('{"invalid": "json').text).toBe('{"invalid": "json'); + }); + + it('should stringify non-string values', () => { + expect(displayJsonValue(field)(42).text).toBe('42'); + }); + + it('should use the underlying field.display method to format values and return numeric values', () => { + field.display = (val: unknown) => ({ text: `**${val}**`, numeric: Number(val), suffix: 'ms' }); + expect(displayJsonValue(field)(42).text).toBe('**42**ms'); + expect(displayJsonValue(field)(42).numeric).toBe(42); + }); + + it('should not mangle objects into [object Object]', () => { + expect(displayJsonValue(field)({ a: 1, b: 2 }).text).toBe('{\n "a": 1,\n "b": 2\n}'); + }); + + it('should render arrays as JSON', () => { + expect(displayJsonValue(field)([1, 2, 3]).text).toBe('[\n 1,\n 2,\n 3\n]'); + }); }); describe('applySort', () => { @@ -1505,4 +1542,152 @@ describe('TableNG utils', () => { expect(parseStyleJson('{"notARealStyle": "someValue"}')).toEqual({ notARealStyle: 'someValue' }); }); }); + + describe('prepareSparklineValue', () => { + it('should return an array of numbers when given an array of numbers', () => { + expect( + prepareSparklineValue([1, 2, 3, 4, 5], { + name: 'test', + type: FieldType.number, + values: [1, 2, 3, 4, 5], + config: {}, + }) + ).toEqual({ + y: { + name: `test-sparkline`, + type: FieldType.number, + values: [1, 2, 3, 4, 5], + config: {}, + }, + }); + }); + + it('should parse the x and y values from a dataframe', () => { + const frame = createDataFrame({ + fields: [ + { name: 'x', type: FieldType.time, values: [0, 1000, 2000, 3000, 4000] }, + { name: 'y', type: FieldType.number, values: [10, 20, 30, 40, 50] }, + ], + }); + expect( + prepareSparklineValue(frame, { + name: 'test', + type: FieldType.frame, + values: [frame], + config: {}, + }) + ).toEqual({ + x: { + name: 'x', + type: FieldType.time, + values: [0, 1000, 2000, 3000, 4000], + config: {}, + }, + y: { + name: 'y', + type: FieldType.number, + values: [10, 20, 30, 40, 50], + config: {}, + }, + }); + }); + + it('should return undefined for non-array and non-dataframe values', () => { + expect( + prepareSparklineValue('not an array or dataframe', { + name: 'test', + type: FieldType.string, + values: ['a', 'b', 'c'], + config: {}, + }) + ).toBeUndefined(); + }); + }); + + describe('buildInspectValue', () => { + const numberFieldWithNulls: Field = { + name: 'numbers-with-nulls', + type: FieldType.number, + values: [0, 1, 2, null, NaN], + config: {}, + }; + const stringField: Field = { + name: 'string', + type: FieldType.string, + values: ['foo', 'bar', 'baz', null], + config: {}, + }; + const jsonStringField: Field = { + ...stringField, + config: { custom: { cellOptions: { type: TableCellDisplayMode.JSONView } } }, + }; + const booleanField: Field = { + name: 'boolean-field', + type: FieldType.boolean, + values: [true, false, true], + config: {}, + }; + const sparklineField: Field = { + name: 'sparkline-field', + type: FieldType.frame, + values: [ + createDataFrame({ + fields: [ + { name: 'x', type: FieldType.time, values: [0, 1000, 2000] }, + { name: 'y', type: FieldType.number, values: [10, 20, 30] }, + ], + }), + ], + config: {}, + }; + const sparklineFieldNoX: Field = { + name: 'sparkline-field-no-x', + type: FieldType.other, + values: [[2, 4, 6, 8, 10]], + config: { + custom: { cellOptions: { type: TableCellDisplayMode.Sparkline } }, + }, + }; + const arrayField: Field = { + name: 'array-field', + type: FieldType.other, + values: [ + ['foo', 'bar', 'baz'], + ['one', 'two', 'three'], + ], + config: {}, + }; + const objectField: Field = { + name: 'array-field', + type: FieldType.other, + values: [ + { foo: true, b: 'baz' }, + { foo: false, b: 'qux' }, + ], + config: {}, + }; + const geoField: Field = { + name: 'geo-field', + type: FieldType.geo, + values: [new Point([0, -74.1])], + config: {}, + }; + it.each([ + { name: 'numbers', input: { valueIdx: 0, field: numberFieldWithNulls } }, + { name: 'string', input: { valueIdx: 0, field: stringField } }, + { name: 'string w/ JSON', input: { valueIdx: 2, field: jsonStringField } }, + { name: 'boolean', input: { valueIdx: 0, field: booleanField } }, + { name: 'NaN', input: { valueIdx: 4, field: numberFieldWithNulls } }, + { name: 'null', input: { valueIdx: 3, field: numberFieldWithNulls } }, + { name: 'null w/ JSON', input: { valueIdx: 3, field: jsonStringField } }, + { name: 'undefined', input: { valueIdx: 6, field: numberFieldWithNulls } }, + { name: 'sparkline', input: { valueIdx: 0, field: sparklineField } }, + { name: 'sparkline (no x)', input: { valueIdx: 0, field: sparklineFieldNoX } }, + { name: 'array', input: { valueIdx: 0, field: arrayField } }, + { name: 'object', input: { valueIdx: 0, field: objectField } }, + { name: 'geo', input: { valueIdx: 0, field: geoField } }, + ])('should handle $name', ({ input: { field, valueIdx = 0 } }) => { + expect(buildInspectValue(field.values[valueIdx], field)).toMatchSnapshot(); + }); + }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index 848f21957b0..eb608ce2548 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -1,5 +1,7 @@ import { Property } from 'csstype'; import memoize from 'micro-memoize'; +import WKT from 'ol/format/WKT'; +import Geometry from 'ol/geom/Geometry'; import { CSSProperties } from 'react'; import { SortColumn } from 'react-data-grid'; import tinycolor from 'tinycolor2'; @@ -15,6 +17,9 @@ import { DisplayValueAlignmentFactors, DataFrame, DisplayProcessor, + isDataFrame, + FieldSparkline, + DecimalCount, } from '@grafana/data'; import { BarGaugeDisplayMode, @@ -25,10 +30,11 @@ import { } from '@grafana/schema'; import { getTextColorForAlphaBackground } from '../../../utils/colors'; +import { TableCellInspectorMode } from '../TableCellInspector'; import { TableCellOptions } from '../types'; import { inferPills } from './Cells/PillCell'; -import { AutoCellRenderer, getCellRenderer } from './Cells/renderers'; +import { AutoCellRenderer, getAutoRendererDisplayMode, getCellRenderer } from './Cells/renderers'; import { COLUMN, TABLE } from './constants'; import { TableRow, @@ -949,15 +955,6 @@ export function getApplyToRowBgFn( } } -/** @internal */ -export function withDataLinksActionsTooltip(field: Field, cellType: TableCellDisplayMode) { - return ( - cellType !== TableCellDisplayMode.DataLinks && - cellType !== TableCellDisplayMode.Actions && - (field.config.links?.length ?? 0) + (field.config.actions?.length ?? 0) > 1 - ); -} - /** @internal */ export function canFieldBeColorized( cellType: TableCellDisplayMode, @@ -970,29 +967,96 @@ export function canFieldBeColorized( ); } -export const displayJsonValue: DisplayProcessor = (value: unknown): DisplayValue => { - let displayValue: string; +export const displayJsonValue: (field: Field) => DisplayProcessor = (field: Field, decimals?: DecimalCount) => { + const origDisplay = field.display!; + return (value: unknown): DisplayValue => { + const displayValue = origDisplay(value, decimals); - // Handle string values that might be JSON - if (typeof value === 'string') { - try { - const parsed = JSON.parse(value); - displayValue = JSON.stringify(parsed, null, ' '); - } catch { - displayValue = value; // Keep original if not valid JSON + let jsonText: string; + if (!Array.isArray(value) && !isPlainObject(value)) { + const formattedValue = formattedValueToString(displayValue); + try { + const parsed = JSON.parse(formattedValue); + jsonText = JSON.stringify(parsed, null, ' '); + } catch { + jsonText = formattedValue; // Keep original if not valid JSON + } + } else { + jsonText = JSON.stringify(value, null, ' '); } - } else { - // For non-string values, stringify them - try { - displayValue = JSON.stringify(value, null, ' '); - } catch (error) { - // Handle circular references or other stringify errors - displayValue = String(value); + + return { ...displayValue, text: jsonText }; + }; +}; + +export function prepareSparklineValue(value: unknown, field: Field): FieldSparkline | undefined { + if (Array.isArray(value)) { + return { + y: { + name: `${field.name}-sparkline`, + type: FieldType.number, + values: value, + config: {}, + }, + }; + } + + if (isDataFrame(value)) { + const timeField = value.fields.find((x) => x.type === FieldType.time); + const numberField = value.fields.find((x) => x.type === FieldType.number); + + if (timeField && numberField) { + return { x: timeField, y: numberField }; } } - return { text: displayValue, numeric: Number.NaN }; -}; + return; +} + +function isPlainObject(value: unknown): value is object { + return typeof value === 'object' && value != null && !Array.isArray(value); +} + +export function buildInspectValue(value: unknown, field: Field): [string, TableCellInspectorMode] { + const cellOptions = getCellOptions(field); + + let inspectValue: string; + let mode = TableCellInspectorMode.text; + + if (field.type === FieldType.geo && value instanceof Geometry) { + inspectValue = new WKT().writeGeometry(value, { + featureProjection: 'EPSG:3857', + dataProjection: 'EPSG:4326', + }); + mode = TableCellInspectorMode.code; + } else if ( + cellOptions.type === TableCellDisplayMode.Sparkline || + getAutoRendererDisplayMode(field) === TableCellDisplayMode.Sparkline + ) { + // rather than JSON.stringify this, manually format it to make the coordinate tuples more legible to the user. + const fieldSparkline = prepareSparklineValue(value, field); + inspectValue = '['; + if (fieldSparkline != null) { + // if an x value exists, render as a tuple [x,y], otherwise just y + const buildValString: (idx: number) => string = + fieldSparkline.x != null + ? (idx) => `[${fieldSparkline.x!.values[idx] ?? 'null'}, ${fieldSparkline.y.values[idx] ?? 'null'}]` + : (idx) => `${fieldSparkline.y.values[idx] ?? 'null'}`; + for (let i = 0; i < fieldSparkline.y.values.length; i++) { + inspectValue += `\n ${buildValString(i)}${i === fieldSparkline.y.values.length - 1 ? '\n' : ','}`; + } + } + inspectValue += ']'; + mode = TableCellInspectorMode.code; + } else if (cellOptions.type === TableCellDisplayMode.JSONView || Array.isArray(value) || isPlainObject(value)) { + inspectValue = JSON.stringify(value, null, ' '); + mode = TableCellInspectorMode.code; + } else { + inspectValue = String(value ?? ''); + } + + return [inspectValue, mode]; +} export function getSummaryCellTextAlign(textAlign: TextAlign, cellType: TableCellDisplayMode): TextAlign { // gauge is weird. left-aligned gauge has the viz on the left and its numbers on the right, and vice-versa. diff --git a/packages/grafana-ui/src/components/Table/TableRT/Table.tsx b/packages/grafana-ui/src/components/Table/TableRT/Table.tsx index 4df81505431..ea909d4642f 100644 --- a/packages/grafana-ui/src/components/Table/TableRT/Table.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/Table.tsx @@ -40,6 +40,11 @@ const COLUMN_MIN_WIDTH = 150; const FOOTER_ROW_HEIGHT = 36; const NO_DATA_TEXT = 'No data'; +/** + * Used for displaying tabular data + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-table--docs + */ export const Table = memo((props: Props) => { const { ariaLabel, diff --git a/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx index 98cc9447030..dfd967e04e3 100644 --- a/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx +++ b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx @@ -88,9 +88,10 @@ export class UnThemedTableInputCSV extends PureComponent { return ( - Rows:{{ rows }}, Columns:{{ columns }}   - + Rows:{{ rows }}, Columns:{{ columns }} +   + ); })} @@ -101,7 +102,11 @@ export class UnThemedTableInputCSV extends PureComponent { } } -/** @deprecated */ +/** + * @deprecated + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-deprecated-tableinputcsv--docs + */ export const TableInputCSV = withTheme2(UnThemedTableInputCSV); TableInputCSV.displayName = 'TableInputCSV'; diff --git a/packages/grafana-ui/src/components/Tabs/Tab.tsx b/packages/grafana-ui/src/components/Tabs/Tab.tsx index 6eb695df437..ae58e136aeb 100644 --- a/packages/grafana-ui/src/components/Tabs/Tab.tsx +++ b/packages/grafana-ui/src/components/Tabs/Tab.tsx @@ -31,6 +31,9 @@ export interface TabProps extends HTMLProps { disabled?: boolean; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/navigation-tabs--docs + */ export const Tab = React.forwardRef( ( { diff --git a/packages/grafana-ui/src/components/Tabs/TabContent.tsx b/packages/grafana-ui/src/components/Tabs/TabContent.tsx index a22f6102dba..9bceb46f3c8 100644 --- a/packages/grafana-ui/src/components/Tabs/TabContent.tsx +++ b/packages/grafana-ui/src/components/Tabs/TabContent.tsx @@ -9,6 +9,9 @@ interface Props extends HTMLAttributes { children: ReactNode; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/navigation-tabs--docs + */ export const TabContent = ({ children, className, ...restProps }: Props) => { const styles = useStyles2(getTabContentStyle); diff --git a/packages/grafana-ui/src/components/Tabs/TabsBar.tsx b/packages/grafana-ui/src/components/Tabs/TabsBar.tsx index 8ba4405dcfa..c33734f7ea9 100644 --- a/packages/grafana-ui/src/components/Tabs/TabsBar.tsx +++ b/packages/grafana-ui/src/components/Tabs/TabsBar.tsx @@ -13,6 +13,11 @@ export interface Props { hideBorder?: boolean; } +/** + * A composition component for rendering a TabBar with Tabs for navigation. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/navigation-tabs--docs + */ export const TabsBar = forwardRef(({ children, className, hideBorder = false }, ref) => { const styles = useStyles2(getStyles); diff --git a/packages/grafana-ui/src/components/Tags/Tag.tsx b/packages/grafana-ui/src/components/Tags/Tag.tsx index e1dde995bad..1d529dfcc24 100644 --- a/packages/grafana-ui/src/components/Tags/Tag.tsx +++ b/packages/grafana-ui/src/components/Tags/Tag.tsx @@ -57,6 +57,11 @@ const TagSkeleton: SkeletonComponent = ({ rootProps }) => { return ; }; +/** + * Used for displaying metadata, for example to add more details to search results. Background and border colors are generated from the tag name. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-tag--docs + */ export const Tag = attachSkeleton(TagComponent, TagSkeleton); const getSkeletonStyles = () => ({ diff --git a/packages/grafana-ui/src/components/Tags/TagList.tsx b/packages/grafana-ui/src/components/Tags/TagList.tsx index d128c64610b..729fc920f28 100644 --- a/packages/grafana-ui/src/components/Tags/TagList.tsx +++ b/packages/grafana-ui/src/components/Tags/TagList.tsx @@ -73,6 +73,11 @@ const TagListSkeleton: SkeletonComponent = ({ rootProps }) => { ); }; +/** + * List of tags with predefined margins and positioning. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/information-taglist--docs + */ export const TagList = attachSkeleton(TagListComponent, TagListSkeleton); const getSkeletonStyles = (theme: GrafanaTheme2) => ({ diff --git a/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx b/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx index 7bcdbb6852a..1b6e78213da 100644 --- a/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx +++ b/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx @@ -29,6 +29,11 @@ export interface Props { autoColors?: boolean; } +/** + * A set of an input field and a button next to it that allows the user to add new tags. The added tags are previewed next to the input and can be removed by clicking the "X" icon. You can customize the width of the input. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-tagsinput--docs + */ export const TagsInput = forwardRef( ( { diff --git a/packages/grafana-ui/src/components/Text/Text.tsx b/packages/grafana-ui/src/components/Text/Text.tsx index 55b88ee4011..56cb2b529bb 100644 --- a/packages/grafana-ui/src/components/Text/Text.tsx +++ b/packages/grafana-ui/src/components/Text/Text.tsx @@ -29,6 +29,11 @@ export interface TextProps extends Omit, 'clas children: NonNullable; } +/** + * The Text component can be used to apply typography styles in a simple way, without the need of extra css. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/foundations-text--docs + */ export const Text = React.forwardRef( ( { element = 'span', variant, weight, color, truncate, italic, textAlignment, children, tabular, ...restProps }, diff --git a/packages/grafana-ui/src/components/TextArea/TextArea.tsx b/packages/grafana-ui/src/components/TextArea/TextArea.tsx index 1c84344348f..6c80d370dc6 100644 --- a/packages/grafana-ui/src/components/TextArea/TextArea.tsx +++ b/packages/grafana-ui/src/components/TextArea/TextArea.tsx @@ -11,6 +11,11 @@ export interface Props extends Omit, 'size'> { invalid?: boolean; } +/** + * Use for multi line inputs like descriptions. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/inputs-textarea--docs + */ export const TextArea = forwardRef(({ invalid, className, ...props }, ref) => { const styles = useStyles2(getTextAreaStyle, invalid); diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx index 991490e36aa..84f13eaf112 100644 --- a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx @@ -48,6 +48,11 @@ export interface ToggletipProps { onOpen?: () => void; } +/** + * Toggletips, similar to Tooltips, provide contextual support for users when needed. They are hidden by default, a UI trigger or text link are clicked to set them to their visible state. Toggletips, unlike tooltips, are persistent until a user takes action to dismiss them by clicking on the required “X” (close) trigger. Toggletips are capable of containing varying types of complex content including interactive components, buttons, and dropdowns. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-toggletip--docs + */ export const Toggletip = memo( ({ children, diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx index 376b1b3fc38..40b468d73fb 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -41,6 +41,11 @@ export type ToolbarButtonProps = CommonProps & ButtonHTMLAttributes( ( { diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButtonRow.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButtonRow.tsx index 000713c57c4..54608a3acbd 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButtonRow.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButtonRow.tsx @@ -17,6 +17,11 @@ export interface Props extends HTMLAttributes { alignment?: 'left' | 'right'; } +/** + * A container for multiple ToolbarButtons. Provides automatic overflow behaviour when the buttons no longer fit in the container. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/navigation-toolbarbuttonrow--docs + */ export const ToolbarButtonRow = forwardRef( ({ alignment = 'left', className, children, ...rest }, ref) => { // null/undefined are valid react children so we need to filter them out to prevent unnecessary padding diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index b44d48ddf38..665e2d7bf6c 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -34,6 +34,9 @@ export interface TooltipProps { interactive?: boolean; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-tooltip--docs + */ export const Tooltip = forwardRef( ({ children, theme, interactive, show, placement, content }, forwardedRef) => { const arrowRef = useRef(null); diff --git a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx index dff45cb2a18..ac53b424bdb 100644 --- a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx +++ b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx @@ -16,6 +16,9 @@ function formatCreateLabel(input: string) { return `Custom unit: ${input}`; } +/** + * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-unitpicker--docs + */ export class UnitPicker extends PureComponent { onChange = (value: SelectableValue) => { this.props.onChange(value.value); diff --git a/packages/grafana-ui/src/components/UsersIndicator/UserIcon.tsx b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.tsx index 3d8dd97f962..ca5ff1f665c 100644 --- a/packages/grafana-ui/src/components/UsersIndicator/UserIcon.tsx +++ b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.tsx @@ -51,6 +51,11 @@ const getUserInitials = (name?: string) => { return `${first?.[0] ?? ''}${last?.[0] ?? ''}`.toUpperCase(); }; +/** + * UserIcon renders a user icon and displays the user's name or initials along with the user's active status or last viewed date. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/iconography-usericon--docs + */ export const UserIcon = ({ userView, className, diff --git a/packages/grafana-ui/src/components/UsersIndicator/UsersIndicator.tsx b/packages/grafana-ui/src/components/UsersIndicator/UsersIndicator.tsx index 44518ce25c7..66c9a0bddca 100644 --- a/packages/grafana-ui/src/components/UsersIndicator/UsersIndicator.tsx +++ b/packages/grafana-ui/src/components/UsersIndicator/UsersIndicator.tsx @@ -16,6 +16,12 @@ export interface UsersIndicatorProps { /** onClick handler for the user number indicator */ onClick?: () => void; } + +/** + * A component that displays a set of user icons indicating which users are currently active. If there are too many users to display all the icons, it will collapse the icons into a single icon with a number indicating the number of additional users. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/iconography-usersindicator--docs + */ export const UsersIndicator = ({ users, onClick, limit = 4 }: UsersIndicatorProps) => { const styles = useStyles2(getStyles); if (!users.length) { diff --git a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx index 128a038ea8d..8647140c615 100644 --- a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx +++ b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx @@ -37,6 +37,11 @@ export interface ValuePickerProps { buttonCss?: string; } +/** + * A component that looks like a button but transforms into a select when clicked. + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-valuepicker--docs + */ export function ValuePicker({ 'aria-label': ariaLabel, label, diff --git a/packages/grafana-ui/src/components/VizLayout/VizLayout.tsx b/packages/grafana-ui/src/components/VizLayout/VizLayout.tsx index 6194b3ac6b2..11b5ed6dc00 100644 --- a/packages/grafana-ui/src/components/VizLayout/VizLayout.tsx +++ b/packages/grafana-ui/src/components/VizLayout/VizLayout.tsx @@ -29,6 +29,8 @@ export interface VizLayoutComponentType extends FC { /** * @beta + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-vizlayout--docs */ export const VizLayout: VizLayoutComponentType = ({ width, height, legend, children }) => { const theme = useTheme2(); diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx index f07c3abff22..0a8dc48d3a9 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx @@ -13,6 +13,8 @@ import { mapMouseEventToMode } from './utils'; /** * @public + * + * https://developers.grafana.com/ui/latest/index.html?path=/docs/plugins-vizlegend--docs */ export function VizLegend({ items, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index 3d8d5972fe1..675d6536184 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -163,8 +163,6 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { if (values) { config.values = values; - } else if (isTime) { - config.values = formatTime; } else if (formatValue) { config.values = (u: uPlot, splits, axisIdx, tickSpace, tickIncr) => { let decimals = guessDecimals(roundDecimals(tickIncr, 6)); @@ -176,6 +174,8 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { } }); }; + } else if (isTime) { + config.values = formatTime; } // store timezone diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 68aa92e6f66..2431d9ecff5 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -286,6 +286,7 @@ type UPlotConfigPrepOpts = {}> = { tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps; hoverProximity?: number; orientation?: VizOrientation; + xAxisConfig?: Pick; } & T; /** @alpha */ diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 17147e8661b..606839aacad 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -340,6 +340,30 @@ export const TooltipPlugin2 = ({ ); } + // add zoom-in cursor during drag-to-zoom interaction + if (queryZoom != null || clientZoom) { + u.over.addEventListener( + 'mousedown', + (e) => { + if (!maybeZoomAction(e)) { + return; + } + + if (e.button === 0) { + u.over.classList.add('zoom-drag'); + + let onUp = () => { + u.over.classList.remove('zoom-drag'); + document.removeEventListener('mouseup', onUp, true); + }; + + document.addEventListener('mouseup', onUp, true); + } + }, + true + ); + } + // this handles pinning, 0-width range selection, and one-click u.over.addEventListener('click', (e) => { if (e.target === u.over) { diff --git a/packages/grafana-ui/src/options/builder/index.ts b/packages/grafana-ui/src/options/builder/index.ts index 98b376794ee..42dd18e4850 100644 --- a/packages/grafana-ui/src/options/builder/index.ts +++ b/packages/grafana-ui/src/options/builder/index.ts @@ -4,4 +4,3 @@ export * from './legend'; export { addTooltipOptions } from './tooltip'; export * from './text'; export * from './stacking'; -export { addTimeCompareOption } from './timeCompare'; diff --git a/packages/grafana-ui/src/options/builder/timeCompare.tsx b/packages/grafana-ui/src/options/builder/timeCompare.tsx deleted file mode 100644 index 222856ff9f4..00000000000 --- a/packages/grafana-ui/src/options/builder/timeCompare.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { PanelOptionsEditorBuilder } from '@grafana/data'; -import { TimeCompareOptions } from '@grafana/schema'; - -/** - * Adds a generic time comparison option to the panel options editor. - * Can be used by any panel that supports time comparison. - */ -export function addTimeCompareOption( - builder: PanelOptionsEditorBuilder, - defaultValue = false -) { - builder.addBooleanSwitch({ - path: 'timeCompare', - name: 'Show comparison selector', - category: ['Time Comparison'], - description: 'Enables the comparison selector, so you can compare data between two time ranges', - defaultValue, - }); -} diff --git a/packages/grafana-ui/src/themes/GlobalStyles/uPlot.ts b/packages/grafana-ui/src/themes/GlobalStyles/uPlot.ts index 3637999adee..ae0e066bbfc 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/uPlot.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/uPlot.ts @@ -12,6 +12,10 @@ export function getUplotStyles(theme: GrafanaTheme2) { background: 'rgba(120, 120, 130, 0.2)', }, + '.u-over.zoom-drag': { + cursor: 'zoom-in', + }, + '.u-hz .u-cursor-x, .u-vt .u-cursor-y': { borderRight: '1px dashed rgba(120, 120, 130, 0.5)', }, diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index a06401c33cb..82afd586730 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -118,18 +118,18 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect + golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index b257d642ece..5d30f79835b 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -314,15 +314,15 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -330,8 +330,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= @@ -355,23 +355,23 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index d324831b075..0578cefd724 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -273,7 +273,7 @@ func setupSimpleHTTPServer(features featuremgmt.FeatureToggles) *HTTPServer { AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), annotationsRepo: annotationstest.NewFakeAnnotationsRepo(), authInfoService: &authinfotest.FakeService{ - ExpectedLabels: map[int64]string{int64(1): login.GetAuthProviderLabel(login.LDAPAuthModule)}, + ExpectedRecentlyUsedLabel: map[int64]string{int64(1): login.GetAuthProviderLabel(login.LDAPAuthModule)}, }, tracer: tracing.InitializeTracerForTest(), } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index aedd222d94b..7bbf48ab0f2 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -438,7 +438,6 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S } ctx = c.Req.Context() - var err error var userID int64 if id, err := identity.UserIdentifier(c.GetID()); err == nil { @@ -518,12 +517,6 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, saveErr) } - // connect library panels for this dashboard after the dashboard is stored and has an ID - err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(ctx, c.SignedInUser, dashboard) - if err != nil { - return response.Error(http.StatusInternalServerError, "Error while connecting library panels", err) - } - c.TimeRequest(metrics.MApiDashboardSave) return response.JSON(http.StatusOK, util.DynMap{ "status": "success", diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 7c47656e208..362be520230 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -20,7 +20,6 @@ import ( "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" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" @@ -39,7 +38,6 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" 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" "github.com/grafana/grafana/pkg/services/org" @@ -265,7 +263,6 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) hs.starService = startest.NewStarServiceFake() - hs.LibraryPanelService = &mockLibraryPanelService{} hs.LibraryElementService = &libraryelementsfake.LibraryElementService{} middleware := publicdashboards.NewFakePublicDashboardMiddleware(t) @@ -791,7 +788,6 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: dashboardService, SQLStore: dbtest.NewFakeDB(), @@ -853,7 +849,6 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { hs := &HTTPServer{ Cfg: setting.NewCfg(), ProvisioningService: fakeProvisioningService, - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: dashboardProvisioningService, SQLStore: mockSQLStore, @@ -887,7 +882,6 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { hs := &HTTPServer{ Cfg: setting.NewCfg(), ProvisioningService: fakeProvisioningService, - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: dashboardProvisioningService, SQLStore: mockSQLStore, @@ -928,7 +922,6 @@ func TestIntegrationDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { hs := &HTTPServer{ Cfg: setting.NewCfg(), - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, SQLStore: mockSQLStore, AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, @@ -1087,7 +1080,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), pluginStore: &pluginstore.FakePluginStore{}, - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: dashboardService, folderService: folderService, @@ -1127,7 +1119,6 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), - LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: mock, SQLStore: sqlStore, @@ -1177,15 +1168,3 @@ func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboard ) { return nil, nil } - -type mockLibraryPanelService struct{} - -var _ librarypanels.Service = (*mockLibraryPanelService)(nil) - -func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error { - return nil -} - -func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { - return nil -} diff --git a/pkg/api/ds_query.go b/pkg/api/ds_query.go index dd33ba97817..6d27fbfc404 100644 --- a/pkg/api/ds_query.go +++ b/pkg/api/ds_query.go @@ -76,6 +76,8 @@ func (hs *HTTPServer) QueryMetricsV2(c *contextmodel.ReqContext) response.Respon var resp *backend.QueryDataResponse var err error + + hs.log.Debug("QueryMetricsV2: request received", "time_in_query", handleTimeInQuery) if handleTimeInQuery { resp, err = hs.queryDataService.QueryDataNew(c.Req.Context(), c.SignedInUser, c.SkipDSCache, reqDTO) } else { diff --git a/pkg/api/ds_query_test.go b/pkg/api/ds_query_test.go index e330f8f4438..11e12800056 100644 --- a/pkg/api/ds_query_test.go +++ b/pkg/api/ds_query_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" @@ -86,6 +87,7 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) { server := SetupAPITestServer(t, func(hs *HTTPServer) { hs.queryDataService = qds hs.QuotaService = quotatest.New(false, nil) + hs.log = log.New("test-logger") }) t.Run("Status code is 400 when data source response has an error", func(t *testing.T) { @@ -252,6 +254,7 @@ func TestDataSourceQueryError(t *testing.T) { err := r.Add(context.Background(), p) require.NoError(t, err) ds := &fakeDatasources.FakeDataSourceService{} + hs.log = log.New("test-logger") hs.queryDataService = query.ProvideService( cfg, &fakeDatasources.FakeCacheService{}, diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 296ab669aa2..4d412a0e271 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -78,7 +78,8 @@ type FrontendSettingsAzureDTO struct { } type FrontendSettingsCachingDTO struct { - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled"` + CleanCacheEnabled bool `json:"cleanCacheEnabled"` } type FrontendSettingsRecordedQueriesDTO struct { @@ -207,27 +208,28 @@ type FrontendSettingsDTO struct { DashboardPerformanceMetrics []string `json:"dashboardPerformanceMetrics"` PanelSeriesLimit int `json:"panelSeriesLimit"` - FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"` - ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"` - ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"` - DisableLoginForm bool `json:"disableLoginForm"` - DisableUserSignUp bool `json:"disableUserSignUp"` - LoginHint string `json:"loginHint"` - PasswordHint string `json:"passwordHint"` - ExternalUserMngInfo string `json:"externalUserMngInfo"` - ExternalUserMngLinkUrl string `json:"externalUserMngLinkUrl"` - ExternalUserMngLinkName string `json:"externalUserMngLinkName"` - ExternalUserMngAnalytics bool `json:"externalUserMngAnalytics"` - ExternalUserMngAnalyticsParams string `json:"externalUserMngAnalyticsParams"` - ViewersCanEdit bool `json:"viewersCanEdit"` - DisableSanitizeHtml bool `json:"disableSanitizeHtml"` - TrustedTypesDefaultPolicyEnabled bool `json:"trustedTypesDefaultPolicyEnabled"` - CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"` - EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"` - PluginRestrictedAPIsAllowList map[string][]string `json:"pluginRestrictedAPIsAllowList"` - PluginRestrictedAPIsBlockList map[string][]string `json:"pluginRestrictedAPIsBlockList"` - ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"` - ExploreHideLogsDownload bool `json:"exploreHideLogsDownload"` + FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"` + ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"` + ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"` + ApplicationInsightsAutoRouteTracking bool `json:"applicationInsightsAutoRouteTracking"` + DisableLoginForm bool `json:"disableLoginForm"` + DisableUserSignUp bool `json:"disableUserSignUp"` + LoginHint string `json:"loginHint"` + PasswordHint string `json:"passwordHint"` + ExternalUserMngInfo string `json:"externalUserMngInfo"` + ExternalUserMngLinkUrl string `json:"externalUserMngLinkUrl"` + ExternalUserMngLinkName string `json:"externalUserMngLinkName"` + ExternalUserMngAnalytics bool `json:"externalUserMngAnalytics"` + ExternalUserMngAnalyticsParams string `json:"externalUserMngAnalyticsParams"` + ViewersCanEdit bool `json:"viewersCanEdit"` + DisableSanitizeHtml bool `json:"disableSanitizeHtml"` + TrustedTypesDefaultPolicyEnabled bool `json:"trustedTypesDefaultPolicyEnabled"` + CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"` + EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"` + PluginRestrictedAPIsAllowList map[string][]string `json:"pluginRestrictedAPIsAllowList"` + PluginRestrictedAPIsBlockList map[string][]string `json:"pluginRestrictedAPIsBlockList"` + ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"` + ExploreHideLogsDownload bool `json:"exploreHideLogsDownload"` Auth FrontendSettingsAuthDTO `json:"auth"` @@ -235,29 +237,30 @@ type FrontendSettingsDTO struct { LicenseInfo FrontendSettingsLicenseInfoDTO `json:"licenseInfo"` - FeatureToggles map[string]bool `json:"featureToggles"` - AnonymousEnabled bool `json:"anonymousEnabled"` - AnonymousDeviceLimit int64 `json:"anonymousDeviceLimit"` - RendererAvailable bool `json:"rendererAvailable"` - RendererVersion string `json:"rendererVersion"` - RendererDefaultImageWidth int `json:"rendererDefaultImageWidth"` - RendererDefaultImageHeight int `json:"rendererDefaultImageHeight"` - RendererDefaultImageScale float64 `json:"rendererDefaultImageScale"` - Http2Enabled bool `json:"http2Enabled"` - GrafanaJavascriptAgent setting.GrafanaJavascriptAgent `json:"grafanaJavascriptAgent"` - PluginCatalogURL string `json:"pluginCatalogURL"` - PluginAdminEnabled bool `json:"pluginAdminEnabled"` - PluginAdminExternalManageEnabled bool `json:"pluginAdminExternalManageEnabled"` - PluginCatalogHiddenPlugins []string `json:"pluginCatalogHiddenPlugins"` - PluginCatalogManagedPlugins []string `json:"pluginCatalogManagedPlugins"` - PluginCatalogPreinstalledPlugins []setting.InstallPlugin `json:"pluginCatalogPreinstalledPlugins"` - ExpressionsEnabled bool `json:"expressionsEnabled"` - AwsAllowedAuthProviders []string `json:"awsAllowedAuthProviders"` - AwsAssumeRoleEnabled bool `json:"awsAssumeRoleEnabled"` - SupportBundlesEnabled bool `json:"supportBundlesEnabled"` - SnapshotEnabled bool `json:"snapshotEnabled"` - SecureSocksDSProxyEnabled bool `json:"secureSocksDSProxyEnabled"` - ReportingStaticContext map[string]string `json:"reportingStaticContext"` + FeatureToggles map[string]bool `json:"featureToggles"` + AnonymousEnabled bool `json:"anonymousEnabled"` + AnonymousDeviceLimit int64 `json:"anonymousDeviceLimit"` + RendererAvailable bool `json:"rendererAvailable"` + RendererVersion string `json:"rendererVersion"` + RendererDefaultImageWidth int `json:"rendererDefaultImageWidth"` + RendererDefaultImageHeight int `json:"rendererDefaultImageHeight"` + RendererDefaultImageScale float64 `json:"rendererDefaultImageScale"` + Http2Enabled bool `json:"http2Enabled"` + GrafanaJavascriptAgent setting.GrafanaJavascriptAgent `json:"grafanaJavascriptAgent"` + PluginCatalogURL string `json:"pluginCatalogURL"` + PluginAdminEnabled bool `json:"pluginAdminEnabled"` + PluginAdminExternalManageEnabled bool `json:"pluginAdminExternalManageEnabled"` + PluginCatalogHiddenPlugins []string `json:"pluginCatalogHiddenPlugins"` + PluginCatalogManagedPlugins []string `json:"pluginCatalogManagedPlugins"` + PluginCatalogPreinstalledPlugins []setting.InstallPlugin `json:"pluginCatalogPreinstalledPlugins"` + PluginCatalogPreinstalledAutoUpdate bool `json:"pluginCatalogPreinstalledAutoUpdate"` + ExpressionsEnabled bool `json:"expressionsEnabled"` + AwsAllowedAuthProviders []string `json:"awsAllowedAuthProviders"` + AwsAssumeRoleEnabled bool `json:"awsAssumeRoleEnabled"` + SupportBundlesEnabled bool `json:"supportBundlesEnabled"` + SnapshotEnabled bool `json:"snapshotEnabled"` + SecureSocksDSProxyEnabled bool `json:"secureSocksDSProxyEnabled"` + ReportingStaticContext map[string]string `json:"reportingStaticContext"` Azure FrontendSettingsAzureDTO `json:"azure"` diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 958113e9f36..851910a9e1d 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -271,26 +271,15 @@ func (hs *HTTPServer) UpdateFolder(c *contextmodel.ReqContext) response.Response // 403: forbiddenError // 404: notFoundError // 500: internalServerError -func (hs *HTTPServer) DeleteFolder(c *contextmodel.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed - err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) +func (hs *HTTPServer) DeleteFolder(c *contextmodel.ReqContext) response.Response { + uid := web.Params(c.Req)[":uid"] + err := hs.folderService.Delete(c.Req.Context(), &folder.DeleteFolderCommand{UID: uid, OrgID: c.GetOrgID(), ForceDeleteRules: c.QueryBool("forceDeleteRules"), SignedInUser: c.SignedInUser}) if err != nil { if errors.Is(err, model.ErrFolderHasConnectedLibraryElements) { return response.Error(http.StatusForbidden, "Folder could not be deleted because it contains library elements in use", err) } return apierrors.ToFolderErrorResponse(err) } - /* TODO: after a decision regarding folder deletion permissions has been made - (https://github.com/grafana/grafana-enterprise/issues/5144), - remove the previous call to hs.LibraryElementService.DeleteLibraryElementsInFolder - and remove "user" from the signature of DeleteInFolder in the folder RegistryService. - Context: https://github.com/grafana/grafana/pull/69149#discussion_r1235057903 - */ - - uid := web.Params(c.Req)[":uid"] - err = hs.folderService.Delete(c.Req.Context(), &folder.DeleteFolderCommand{UID: uid, OrgID: c.GetOrgID(), ForceDeleteRules: c.QueryBool("forceDeleteRules"), SignedInUser: c.SignedInUser}) - if err != nil { - return apierrors.ToFolderErrorResponse(err) - } return response.JSON(http.StatusOK, util.DynMap{ "message": "Folder deleted", diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 43efa380f5c..2895fd14cf7 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -197,53 +197,54 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro featureToggles["topnav"] = true frontendSettings := &dtos.FrontendSettingsDTO{ - DefaultDatasource: defaultDS, - Datasources: dataSources, - MinRefreshInterval: hs.Cfg.MinRefreshInterval, - Panels: panels, - Apps: apps, - AppUrl: hs.Cfg.AppURL, - AppSubUrl: hs.Cfg.AppSubURL, - AllowOrgCreate: (hs.Cfg.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, - AuthProxyEnabled: hs.Cfg.AuthProxy.Enabled, - LdapEnabled: hs.Cfg.LDAPAuthEnabled, - JwtHeaderName: hs.Cfg.JWTAuth.HeaderName, - JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin, - LiveEnabled: hs.Cfg.LiveMaxConnections != 0, - LiveMessageSizeLimit: hs.Cfg.LiveMessageSizeLimit, - AutoAssignOrg: hs.Cfg.AutoAssignOrg, - VerifyEmailEnabled: hs.Cfg.VerifyEmailEnabled, - SigV4AuthEnabled: hs.Cfg.SigV4AuthEnabled, - AzureAuthEnabled: hs.Cfg.AzureAuthEnabled, - RbacEnabled: true, - ExploreEnabled: hs.Cfg.ExploreEnabled, - HelpEnabled: hs.Cfg.HelpEnabled, - ProfileEnabled: hs.Cfg.ProfileEnabled, - NewsFeedEnabled: hs.Cfg.NewsFeedEnabled, - QueryHistoryEnabled: hs.Cfg.QueryHistoryEnabled, - GoogleAnalyticsId: hs.Cfg.GoogleAnalyticsID, - GoogleAnalytics4Id: hs.Cfg.GoogleAnalytics4ID, - GoogleAnalytics4SendManualPageViews: hs.Cfg.GoogleAnalytics4SendManualPageViews, - RudderstackWriteKey: hs.Cfg.RudderstackWriteKey, - RudderstackDataPlaneUrl: hs.Cfg.RudderstackDataPlaneURL, - RudderstackSdkUrl: hs.Cfg.RudderstackSDKURL, - RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL, - RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL, - AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting, - DashboardPerformanceMetrics: hs.Cfg.DashboardPerformanceMetrics, - PanelSeriesLimit: hs.Cfg.PanelSeriesLimit, - FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled, - ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString, - ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl, - DisableLoginForm: hs.Cfg.DisableLoginForm, - DisableUserSignUp: !hs.Cfg.AllowUserSignUp, - LoginHint: hs.Cfg.LoginHint, - PasswordHint: hs.Cfg.PasswordHint, - ExternalUserMngInfo: hs.Cfg.ExternalUserMngInfo, - ExternalUserMngLinkUrl: hs.Cfg.ExternalUserMngLinkUrl, - ExternalUserMngLinkName: hs.Cfg.ExternalUserMngLinkName, - ExternalUserMngAnalytics: hs.Cfg.ExternalUserMngAnalytics, - ExternalUserMngAnalyticsParams: hs.Cfg.ExternalUserMngAnalyticsParams, + DefaultDatasource: defaultDS, + Datasources: dataSources, + MinRefreshInterval: hs.Cfg.MinRefreshInterval, + Panels: panels, + Apps: apps, + AppUrl: hs.Cfg.AppURL, + AppSubUrl: hs.Cfg.AppSubURL, + AllowOrgCreate: (hs.Cfg.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, + AuthProxyEnabled: hs.Cfg.AuthProxy.Enabled, + LdapEnabled: hs.Cfg.LDAPAuthEnabled, + JwtHeaderName: hs.Cfg.JWTAuth.HeaderName, + JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin, + LiveEnabled: hs.Cfg.LiveMaxConnections != 0, + LiveMessageSizeLimit: hs.Cfg.LiveMessageSizeLimit, + AutoAssignOrg: hs.Cfg.AutoAssignOrg, + VerifyEmailEnabled: hs.Cfg.VerifyEmailEnabled, + SigV4AuthEnabled: hs.Cfg.SigV4AuthEnabled, + AzureAuthEnabled: hs.Cfg.AzureAuthEnabled, + RbacEnabled: true, + ExploreEnabled: hs.Cfg.ExploreEnabled, + HelpEnabled: hs.Cfg.HelpEnabled, + ProfileEnabled: hs.Cfg.ProfileEnabled, + NewsFeedEnabled: hs.Cfg.NewsFeedEnabled, + QueryHistoryEnabled: hs.Cfg.QueryHistoryEnabled, + GoogleAnalyticsId: hs.Cfg.GoogleAnalyticsID, + GoogleAnalytics4Id: hs.Cfg.GoogleAnalytics4ID, + GoogleAnalytics4SendManualPageViews: hs.Cfg.GoogleAnalytics4SendManualPageViews, + RudderstackWriteKey: hs.Cfg.RudderstackWriteKey, + RudderstackDataPlaneUrl: hs.Cfg.RudderstackDataPlaneURL, + RudderstackSdkUrl: hs.Cfg.RudderstackSDKURL, + RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL, + RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL, + AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting, + DashboardPerformanceMetrics: hs.Cfg.DashboardPerformanceMetrics, + PanelSeriesLimit: hs.Cfg.PanelSeriesLimit, + FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled, + ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString, + ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl, + ApplicationInsightsAutoRouteTracking: hs.Cfg.ApplicationInsightsAutoRouteTracking, + DisableLoginForm: hs.Cfg.DisableLoginForm, + DisableUserSignUp: !hs.Cfg.AllowUserSignUp, + LoginHint: hs.Cfg.LoginHint, + PasswordHint: hs.Cfg.PasswordHint, + ExternalUserMngInfo: hs.Cfg.ExternalUserMngInfo, + ExternalUserMngLinkUrl: hs.Cfg.ExternalUserMngLinkUrl, + ExternalUserMngLinkName: hs.Cfg.ExternalUserMngLinkName, + ExternalUserMngAnalytics: hs.Cfg.ExternalUserMngAnalytics, + ExternalUserMngAnalyticsParams: hs.Cfg.ExternalUserMngAnalyticsParams, //nolint:staticcheck // ViewersCanEdit is deprecated but still used for backward compatibility ViewersCanEdit: hs.Cfg.ViewersCanEdit, DisableSanitizeHtml: hs.Cfg.DisableSanitizeHtml, @@ -290,26 +291,27 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro EnabledFeatures: hs.License.EnabledFeatures(), }, - FeatureToggles: featureToggles, - AnonymousEnabled: hs.Cfg.Anonymous.Enabled, - AnonymousDeviceLimit: hs.Cfg.Anonymous.DeviceLimit, - RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()), - RendererVersion: hs.RenderService.Version(), - RendererDefaultImageWidth: hs.Cfg.RendererDefaultImageWidth, - RendererDefaultImageHeight: hs.Cfg.RendererDefaultImageHeight, - RendererDefaultImageScale: hs.Cfg.RendererDefaultImageScale, - Http2Enabled: hs.Cfg.Protocol == setting.HTTP2Scheme, - GrafanaJavascriptAgent: hs.Cfg.GrafanaJavascriptAgent, - PluginCatalogURL: hs.Cfg.PluginCatalogURL, - PluginAdminEnabled: hs.Cfg.PluginAdminEnabled, - PluginAdminExternalManageEnabled: hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled, - PluginCatalogHiddenPlugins: hs.Cfg.PluginCatalogHiddenPlugins, - PluginCatalogManagedPlugins: hs.managedPluginsService.ManagedPlugins(c.Req.Context()), - PluginCatalogPreinstalledPlugins: append(hs.Cfg.PreinstallPluginsAsync, hs.Cfg.PreinstallPluginsSync...), - ExpressionsEnabled: hs.Cfg.ExpressionsEnabled, - AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders, - AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled, - SupportBundlesEnabled: isSupportBundlesEnabled(hs), + FeatureToggles: featureToggles, + AnonymousEnabled: hs.Cfg.Anonymous.Enabled, + AnonymousDeviceLimit: hs.Cfg.Anonymous.DeviceLimit, + RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()), + RendererVersion: hs.RenderService.Version(), + RendererDefaultImageWidth: hs.Cfg.RendererDefaultImageWidth, + RendererDefaultImageHeight: hs.Cfg.RendererDefaultImageHeight, + RendererDefaultImageScale: hs.Cfg.RendererDefaultImageScale, + Http2Enabled: hs.Cfg.Protocol == setting.HTTP2Scheme, + GrafanaJavascriptAgent: hs.Cfg.GrafanaJavascriptAgent, + PluginCatalogURL: hs.Cfg.PluginCatalogURL, + PluginAdminEnabled: hs.Cfg.PluginAdminEnabled, + PluginAdminExternalManageEnabled: hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled, + PluginCatalogHiddenPlugins: hs.Cfg.PluginCatalogHiddenPlugins, + PluginCatalogManagedPlugins: hs.managedPluginsService.ManagedPlugins(c.Req.Context()), + PluginCatalogPreinstalledPlugins: append(hs.Cfg.PreinstallPluginsAsync, hs.Cfg.PreinstallPluginsSync...), + PluginCatalogPreinstalledAutoUpdate: hs.Cfg.PreinstallAutoUpdate, + ExpressionsEnabled: hs.Cfg.ExpressionsEnabled, + AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders, + AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled, + SupportBundlesEnabled: isSupportBundlesEnabled(hs), Azure: dtos.FrontendSettingsAzureDTO{ Cloud: hs.Cfg.Azure.Cloud, @@ -322,7 +324,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro }, Caching: dtos.FrontendSettingsCachingDTO{ - Enabled: hs.Cfg.SectionWithEnvOverrides("caching").Key("enabled").MustBool(true), + Enabled: hs.Cfg.SectionWithEnvOverrides("caching").Key("enabled").MustBool(true), + CleanCacheEnabled: hs.Cfg.SectionWithEnvOverrides("caching").Key("clean_cache_enabled").MustBool(true), }, RecordedQueries: dtos.FrontendSettingsRecordedQueriesDTO{ Enabled: hs.Cfg.SectionWithEnvOverrides("recorded_queries").Key("enabled").MustBool(true), diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index d8b252ba406..2d3bd11ab37 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -164,7 +164,6 @@ type HTTPServer struct { LoggerMiddleware loggermw.Logger SQLStore db.DB AlertNG *ngalert.AlertNG - LibraryPanelService librarypanels.Service LibraryElementService libraryelements.Service SocialService social.Service Listener net.Listener @@ -319,7 +318,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi ContextHandler: contextHandler, LoggerMiddleware: loggerMiddleware, AlertNG: alertNG, - LibraryPanelService: libraryPanelService, LibraryElementService: libraryElementService, QuotaService: quotaService, tracer: tracer, diff --git a/pkg/api/index.go b/pkg/api/index.go index 765a40c4688..1f85f2b0dbe 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "html/template" "net/http" "strings" @@ -184,12 +185,12 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV NewGrafanaVersionExists: hs.grafanaUpdateChecker.UpdateAvailable(), AppName: setting.ApplicationName, AppNameBodyClass: "app-grafana", - FavIcon: "public/img/fav32.png", - AppleTouchIcon: "public/img/apple-touch-icon.png", + FavIcon: template.URL(assets.ContentDeliveryURL + "public/build/img/fav32.png"), // #nosec G203 + AppleTouchIcon: template.URL(assets.ContentDeliveryURL + "public/build/img/apple-touch-icon.png"), // #nosec G203 AppTitle: "Grafana", NavTree: navTree, Nonce: c.RequestNonce, - LoadingLogo: "public/img/grafana_icon.svg", + LoadingLogo: template.URL(assets.ContentDeliveryURL + "public/build/img/grafana_icon.svg"), // #nosec G203 IsDevelopmentEnv: hs.Cfg.Env == setting.Dev, Assets: assets, } @@ -215,8 +216,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV hs.HooksService.RunIndexDataHooks(&data, c) - data.NavTree.ApplyCostManagementIA() - data.NavTree.ApplyHelpVersion(data.Settings.BuildInfo.VersionString) // RunIndexDataHooks can modify the version string data.NavTree.Sort() return &data, nil diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 0367f58b2f5..8a10cc24944 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -314,7 +314,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or filteredUsers = append(filteredUsers, user) } - modules, err := hs.authInfoService.GetUserLabels(c.Req.Context(), login.GetUserLabelsQuery{ + modules, err := hs.authInfoService.GetUsersRecentlyUsedLabel(c.Req.Context(), login.GetUserLabelsQuery{ UserIDs: authLabelsUserIDs, }) diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 4108bba14f9..632646c7c61 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -144,13 +144,8 @@ type playlistK8sHandler struct { //----------------------------------------------------------------------------------------- func newPlaylistK8sHandler(hs *HTTPServer) *playlistK8sHandler { - gvr := schema.GroupVersionResource{ - Group: v0alpha1.PlaylistKind().Group(), - Version: v0alpha1.PlaylistKind().Version(), - Resource: v0alpha1.PlaylistKind().Plural(), - } return &playlistK8sHandler{ - gvr: gvr, + gvr: v0alpha1.PlaylistKind().GroupVersionResource(), namespacer: request.GetNamespaceMapper(hs.Cfg), clientConfigProvider: hs.clientConfigProvider, } diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 5f0412b5c4c..0ee413447da 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -173,7 +173,7 @@ func TestIntegrationCallResource(t *testing.T) { Backend: true, }, })) - middlewares := pluginsintegration.CreateMiddlewares(cfg, &oauthtokentest.Service{}, tracing.InitializeTracerForTest(), &caching.OSSCachingService{}, featuremgmt.WithFeatures(), prometheus.DefaultRegisterer, pluginRegistry) + middlewares := pluginsintegration.CreateMiddlewares(cfg, &oauthtokentest.Service{}, tracing.InitializeTracerForTest(), caching.ProvideCachingServiceClient(&caching.OSSCachingService{}, nil), featuremgmt.WithFeatures(), prometheus.DefaultRegisterer, pluginRegistry) pc, err := backend.HandlerFromMiddlewares(&pluginfakes.FakePluginClient{ CallResourceHandlerFunc: backend.CallResourceHandlerFunc(func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { diff --git a/pkg/api/short_url.go b/pkg/api/short_url.go index 763c58395ad..7b17fdb01a7 100644 --- a/pkg/api/short_url.go +++ b/pkg/api/short_url.go @@ -11,7 +11,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" - "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" + "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -119,13 +119,8 @@ type shortURLK8sHandler struct { } func newShortURLK8sHandler(hs *HTTPServer) *shortURLK8sHandler { - gvr := schema.GroupVersionResource{ - Group: v1alpha1.ShortURLKind().Group(), - Version: v1alpha1.ShortURLKind().Version(), - Resource: v1alpha1.ShortURLKind().Plural(), - } return &shortURLK8sHandler{ - gvr: gvr, + gvr: v1beta1.ShortURLKind().GroupVersionResource(), namespacer: request.GetNamespaceMapper(hs.Cfg), clientConfigProvider: hs.clientConfigProvider, cfg: hs.Cfg, @@ -169,9 +164,9 @@ func (sk8s *shortURLK8sHandler) getKubernetesRedirectFromShortURL(c *contextmode } result := client.RESTClient().Get(). - Prefix("apis", v1alpha1.APIGroup, v1alpha1.APIVersion). + Prefix("apis", v1beta1.APIGroup, v1beta1.APIVersion). Namespace(sk8s.namespacer(c.OrgID)). - Resource(v1alpha1.ShortURLKind().Plural()). + Resource(v1beta1.ShortURLKind().Plural()). Name(uid). SubResource("goto"). Param("redirect", "false"). // returns the URL and then we will do the redirect @@ -188,7 +183,7 @@ func (sk8s *shortURLK8sHandler) getKubernetesRedirectFromShortURL(c *contextmode return } - value := &v1alpha1.GetGoto{} + value := &v1beta1.GetGoto{} if err = json.Unmarshal(body, value); err != nil { c.JsonApiErr(500, "unmarshal", err) return @@ -217,7 +212,7 @@ func (sk8s *shortURLK8sHandler) createKubernetesShortURLsHandler(c *contextmodel c.Logger.Debug("Creating short URL", "path", cmd.Path) obj := shorturl.LegacyCreateCommandToUnstructured(cmd) - obj.SetGenerateName("u") // becomes a prefix + obj.SetGenerateName("s") // becomes a prefix out, err := client.Create(c.Req.Context(), &obj, v1.CreateOptions{}) if err != nil { diff --git a/pkg/api/short_url_test.go b/pkg/api/short_url_test.go index 6d3da5fbe0c..116d4f70328 100644 --- a/pkg/api/short_url_test.go +++ b/pkg/api/short_url_test.go @@ -11,6 +11,7 @@ import ( "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" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/shorturls" @@ -31,7 +32,7 @@ func TestShortURLAPIEndpoint(t *testing.T) { Path: cmd.Path, } service := &fakeShortURLService{ - createShortURLFunc: func(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) { + createShortURLFunc: func(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) { return createResp, nil }, createConvertShortURLToDTO: func(shortURL *shorturls.ShortUrl, appURL string) *dtos.ShortURL { @@ -81,7 +82,7 @@ func createShortURLScenario(t *testing.T, desc string, url string, routePattern } type fakeShortURLService struct { - createShortURLFunc func(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) + createShortURLFunc func(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) createConvertShortURLToDTO func(shortURL *shorturls.ShortUrl, appURL string) *dtos.ShortURL } @@ -89,11 +90,11 @@ func (s *fakeShortURLService) List(ctx context.Context, orgID int64) ([]*shortur return nil, nil } -func (s *fakeShortURLService) GetShortURLByUID(ctx context.Context, user *user.SignedInUser, uid string) (*shorturls.ShortUrl, error) { +func (s *fakeShortURLService) GetShortURLByUID(ctx context.Context, user identity.Requester, uid string) (*shorturls.ShortUrl, error) { return nil, nil } -func (s *fakeShortURLService) CreateShortURL(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) { +func (s *fakeShortURLService) CreateShortURL(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) { if s.createShortURLFunc != nil { return s.createShortURLFunc(ctx, user, cmd) } diff --git a/pkg/api/user.go b/pkg/api/user.go index 311f80e49ae..e8c02ed1cf0 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -115,6 +115,7 @@ func (hs *HTTPServer) GetUserByLoginOrEmail(c *contextmodel.ReqContext) response } return response.Error(http.StatusInternalServerError, "Failed to get user", err) } + result := user.UserProfileDTO{ ID: usr.ID, UID: usr.UID, @@ -128,6 +129,11 @@ func (hs *HTTPServer) GetUserByLoginOrEmail(c *contextmodel.ReqContext) response UpdatedAt: usr.Updated, CreatedAt: usr.Created, } + // Populate AuthLabels using all historically used auth modules ordered by most recent. + if modules, err := hs.authInfoService.GetUserAuthModuleLabels(c.Req.Context(), usr.ID); err == nil { + result.AuthLabels = modules + } + return response.JSON(http.StatusOK, &result) } diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 41b11e76487..8e4b3503015 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -185,6 +185,44 @@ func TestIntegrationUserAPIEndpoint_userLoggedIn(t *testing.T) { require.NoError(t, err) }, mock) + // Multiple historical auth labels should appear ordered by recency + loggedInUserScenario(t, "When calling GET returns with multiple auth labels", "/api/users/lookup", "/api/users/lookup", func(sc *scenarioContext) { + createUserCmd := user.CreateUserCommand{ + Email: fmt.Sprint("multi", "@test.com"), + Name: "multi", + Login: "multi", + IsAdmin: true, + } + orgSvc, err := orgimpl.ProvideService(sqlStore, sc.cfg, quotatest.New(false, nil)) + require.NoError(t, err) + userSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, sc.cfg, nil, nil, tracing.InitializeTracerForTest(), + quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), + ) + require.NoError(t, err) + usr, err := userSvc.Create(context.Background(), &createUserCmd) + require.Nil(t, err) + + sc.handlerFunc = hs.GetUserByLoginOrEmail + + userMock := usertest.NewUserServiceFake() + userMock.ExpectedUser = &user.User{ID: usr.ID, Email: usr.Email, Login: usr.Login, Name: usr.Name} + sc.userService = userMock + hs.userService = userMock + + fakeAuth := &authinfotest.FakeService{ExpectedAuthModuleLabels: []string{login.GetAuthProviderLabel(login.OktaAuthModule), login.GetAuthProviderLabel(login.LDAPAuthModule), login.GetAuthProviderLabel(login.SAMLAuthModule)}} + hs.authInfoService = fakeAuth + + sc.fakeReqWithParams("GET", sc.url, map[string]string{"loginOrEmail": usr.Email}).exec() + + var resp user.UserProfileDTO + require.Equal(t, http.StatusOK, sc.resp.Code) + err = json.Unmarshal(sc.resp.Body.Bytes(), &resp) + require.NoError(t, err) + expected := []string{login.GetAuthProviderLabel(login.OktaAuthModule), login.GetAuthProviderLabel(login.LDAPAuthModule), login.GetAuthProviderLabel(login.SAMLAuthModule)} + require.Equal(t, expected, resp.AuthLabels) + }, mock) + loggedInUserScenario(t, "When calling GET on", "/api/users", "/api/users", func(sc *scenarioContext) { userMock.ExpectedSearchUsers = mockResult diff --git a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go index db4c2e73302..3046d898049 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go +++ b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go @@ -52,7 +52,7 @@ func (u *Unstructured) SetUnstructuredContent(content map[string]any) { // MarshalJSON ensures that the unstructured object produces proper // JSON when passed to Go's standard JSON library. -func (u *Unstructured) MarshalJSON() ([]byte, error) { +func (u Unstructured) MarshalJSON() ([]byte, error) { return json.Marshal(u.Object) } diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 2ba49864c50..6ed1a0f5054 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -45,8 +45,8 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 5ed8ffe6086..e5f34d1e058 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -96,16 +96,16 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 664f519dd8f..00488ed9d85 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -74,6 +74,21 @@ func newInternalIdentity(name string, namespace string, orgID int64, opts ...Ide return staticRequester } +// WithServiceIdentityForSingleNamespace sets an identity representing the service itself in provided namespace and store it in context. +// This is useful for background tasks that has to communicate with other services in the same namespace. It also returns a Requester with +// static permissions so it can be used in legacy code paths. +func WithServiceIdentityForSingleNamespace(ctx context.Context, namespace string, opts ...IdentityOpts) (context.Context, Requester) { + r := newInternalIdentity(serviceName, namespace, 1, opts...) + return WithRequester(ctx, r), r +} + +// WithServiceIdentityForSingleNamespaceContext sets an identity representing the service itself in context, restricted to a namespace. +// Use when using a middleware that signs tokens with the same restriction. +func WithServiceIdentityForSingleNamespaceContext(ctx context.Context, namespace string, opts ...IdentityOpts) context.Context { + ctx, _ = WithServiceIdentityForSingleNamespace(ctx, namespace, opts...) + return ctx +} + // WithServiceIdentity sets an identity representing the service itself in provided org and store it in context. // This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with // static permissions so it can be used in legacy code paths. @@ -125,6 +140,10 @@ var serviceIdentityPermissions = getWildcardPermissions( "datasources:query", "datasources:read", "datasources:delete", + "library.panels:create", // ActionLibraryPanelsCreate + "library.panels:read", // ActionLibraryPanelsRead + "library.panels:write", // ActionLibraryPanelsWrite + "library.panels:delete", // ActionLibraryPanelsDelete "alert.provisioning:write", "alert.provisioning.secrets:read", "users:read", // accesscontrol.ActionUsersRead, diff --git a/pkg/apis/query/v0alpha1/query.go b/pkg/apis/query/v0alpha1/query.go index ef40c17ff45..f6ad172c348 100644 --- a/pkg/apis/query/v0alpha1/query.go +++ b/pkg/apis/query/v0alpha1/query.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "github.com/grafana/grafana/pkg/expr" ) // Generic query request with shared time across all values @@ -28,6 +29,14 @@ type QueryDataResponse struct { backend.QueryDataResponse `json:",inline"` } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type SQLSchemas struct { + metav1.TypeMeta `json:",inline"` + + // Backend wrapper (external dependency) + expr.SQLSchemas `json:"sqlSchemas,inline"` +} + // GetResponseCode return the right status code for the response by checking the responses. func GetResponseCode(rsp *backend.QueryDataResponse) int { if rsp == nil { diff --git a/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go index 38a7bd0b115..ad159a4a6cb 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go @@ -262,3 +262,29 @@ func (in *QueryTypeDefinitionList) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SQLSchemas) DeepCopyInto(out *SQLSchemas) { + *out = *in + out.TypeMeta = in.TypeMeta + out.SQLSchemas = in.SQLSchemas.DeepCopy() + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLSchemas. +func (in *SQLSchemas) DeepCopy() *SQLSchemas { + if in == nil { + return nil + } + out := new(SQLSchemas) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SQLSchemas) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/apis/query/v0alpha1/zz_generated.openapi.go b/pkg/apis/query/v0alpha1/zz_generated.openapi.go index 2ee9ed95395..68f42312ce9 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/query/v0alpha1/zz_generated.openapi.go @@ -23,6 +23,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse": schema_pkg_apis_query_v0alpha1_QueryDataResponse(ref), "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition": schema_pkg_apis_query_v0alpha1_QueryTypeDefinition(ref), "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinitionList": schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.SQLSchemas": schema_pkg_apis_query_v0alpha1_SQLSchemas(ref), } } @@ -482,3 +483,29 @@ func schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref common.Reference "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } + +func schema_pkg_apis_query_v0alpha1_SQLSchemas(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 7d6f606ecfc..c33ac35fd40 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 - github.com/grafana/grafana-app-sdk/logging v0.46.0 + github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 @@ -84,14 +84,15 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 13a308081e6..5c06779f641 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -71,8 +71,8 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8= -github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= @@ -207,8 +207,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -219,8 +219,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= @@ -237,21 +237,21 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/build/daggerbuild/e2e/validate_package.go b/pkg/build/daggerbuild/e2e/validate_package.go index 3bcc1c5f936..ce8f1e97026 100644 --- a/pkg/build/daggerbuild/e2e/validate_package.go +++ b/pkg/build/daggerbuild/e2e/validate_package.go @@ -15,5 +15,6 @@ func ValidatePackage(ctx context.Context, d *dagger.Client, service *dagger.Serv return c.WithServiceBinding("grafana", service). WithEnvVariable("GRAFANA_URL", "http://grafana:3000"). + WithEnvVariable("PW_TEST_HTML_REPORT_OPEN", "never"). WithExec([]string{"yarn", "e2e:acceptance"}), nil } diff --git a/pkg/build/go.mod b/pkg/build/go.mod index bfa8a8c4735..43601bf615f 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -5,7 +5,7 @@ go 1.25.3 // Override docker/docker to avoid: // go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires // github.com/docker/docker@v0.0.0-00010101000000-000000000000: invalid version: unknown revision 000000000000 -replace github.com/docker/docker => github.com/moby/moby v27.5.1+incompatible +replace github.com/docker/docker => github.com/moby/moby v28.0.1+incompatible require ( github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group @@ -13,7 +13,7 @@ require ( go.opentelemetry.io/otel v1.38.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.38.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/trace v1.38.0 // indirect; @grafana/grafana-backend-group - golang.org/x/net v0.45.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources + golang.org/x/net v0.46.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/sync v0.17.0 // @grafana/alerting-backend golang.org/x/text v0.30.0 // indirect; @grafana/grafana-backend-group google.golang.org/grpc v1.76.0 // indirect; @grafana/plugins-platform-backend @@ -38,7 +38,7 @@ require ( dagger.io/dagger v0.18.8 github.com/Masterminds/semver v1.5.0 github.com/quasilyte/go-ruleguard/dsl v0.3.22 - github.com/urfave/cli/v3 v3.4.1 + github.com/urfave/cli/v3 v3.5.0 ) require ( diff --git a/pkg/build/go.sum b/pkg/build/go.sum index 31387b2c885..51f1b7868aa 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -53,8 +53,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= -github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw= +github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= @@ -95,8 +95,8 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index 88e7efbc732..421c9a62583 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -6,10 +6,10 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/subcommands v1.2.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - golang.org/x/tools v0.37.0 + golang.org/x/tools v0.38.0 ) require ( - golang.org/x/mod v0.28.0 // indirect + golang.org/x/mod v0.29.0 // indirect golang.org/x/sync v0.17.0 // indirect ) diff --git a/pkg/build/wire/go.sum b/pkg/build/wire/go.sum index e4262703495..5b0e5da275a 100644 --- a/pkg/build/wire/go.sum +++ b/pkg/build/wire/go.sum @@ -4,9 +4,9 @@ github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 6489520cdea..f17dead4c51 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -6,10 +6,10 @@ require ( cuelang.org/go v0.11.1 github.com/dave/dst v0.27.3 github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d - github.com/grafana/cog v0.0.43 + github.com/grafana/cog v0.0.44 github.com/grafana/cuetsy v0.1.11 github.com/matryer/is v1.4.1 - golang.org/x/tools v0.37.0 + golang.org/x/tools v0.38.0 ) require ( @@ -47,8 +47,8 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/yalue/merged_fs v1.3.0 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/text v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index e5864b3b7d9..afbaacb7ae9 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -31,8 +31,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cog v0.0.43 h1:6EDzJVc8hbP3+AjPnRnAK6mKfyWgqLKbi/jmiStilFk= -github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/cog v0.0.44 h1:N8UP7g6XBHZXf1wY7AOOWC2HdqlTPBJPJiAZQrkf4XQ= +github.com/grafana/cog v0.0.44/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk= @@ -100,16 +100,16 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY= github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/expr/sql/db_test.go b/pkg/expr/sql/db_test.go index ff5ecac022f..c67c23b32b9 100644 --- a/pkg/expr/sql/db_test.go +++ b/pkg/expr/sql/db_test.go @@ -278,18 +278,6 @@ func TestNaNBecomesNull(t *testing.T) { require.NoError(t, err) } -func TestErrorsFromGoMySQLServerAreFlagged(t *testing.T) { - const GmsNotImplemented = "TRUNCATE" // not implemented in go-mysql-server as of 2025-04-11 - - db := DB{} - - query := `SELECT ` + GmsNotImplemented + `(123.456, 2);` - - _, err := db.QueryFrames(context.Background(), &testTracer{}, "sqlExpressionRefId", query, nil) - require.Error(t, err) - require.Contains(t, err.Error(), "error from the sql expression engine") -} - func TestFrameToSQLAndBack_JSONRoundtrip(t *testing.T) { expectedFrame := &data.Frame{ RefID: "json_test", diff --git a/pkg/expr/sql/frame_db_conf_arm.go b/pkg/expr/sql/frame_db_conf_arm.go new file mode 100644 index 00000000000..eaa83ed61df --- /dev/null +++ b/pkg/expr/sql/frame_db_conf_arm.go @@ -0,0 +1,19 @@ +//go:build arm + +package sql + +import ( + "errors" + + mysql "github.com/dolthub/go-mysql-server/sql" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +// TODO: Implement for 32-bit arm +func MySQLColToFieldType(col *mysql.Column) (data.FieldType, error) { + return data.FieldTypeUnknown, errors.New("arm not implemented") +} + +func SchemaFromFrame(frame *data.Frame) mysql.Schema { + return mysql.Schema{} +} diff --git a/pkg/expr/sql/frame_table.go b/pkg/expr/sql/frame_table.go index 2ac897b5007..245efc6e655 100644 --- a/pkg/expr/sql/frame_table.go +++ b/pkg/expr/sql/frame_table.go @@ -30,7 +30,7 @@ func (ft *FrameTable) String() string { return ft.Name() } -func schemaFromFrame(frame *data.Frame) mysql.Schema { +func SchemaFromFrame(frame *data.Frame) mysql.Schema { schema := make(mysql.Schema, len(frame.Fields)) for i, field := range frame.Fields { @@ -48,7 +48,7 @@ func schemaFromFrame(frame *data.Frame) mysql.Schema { // Schema implements the mysql.Table interface func (ft *FrameTable) Schema() mysql.Schema { if ft.schema == nil { - ft.schema = schemaFromFrame(ft.Frame) + ft.schema = SchemaFromFrame(ft.Frame) } return ft.schema } diff --git a/pkg/expr/sql/parser_allow.go b/pkg/expr/sql/parser_allow.go index 6f8cfc1d063..4bac508b1a4 100644 --- a/pkg/expr/sql/parser_allow.go +++ b/pkg/expr/sql/parser_allow.go @@ -183,19 +183,25 @@ func allowedFunction(f *sqlparser.FuncExpr) (b bool) { // Conditional functions case "if", "coalesce", "ifnull", "nullif": return + case "least": + return // Aggregation functions case "sum", "avg", "count", "min", "max": return - case "stddev", "std", "stddev_pop": + case "stddev", "std", "stddev_pop", "stddev_sample": return - case "variance", "var_pop": + case "variance", "var_pop", "var_samp": return case "group_concat": return - case "row_number", "rank", "dense_rank", "lead", "lag": + + // Window Functions + case "row_number", "rank", "dense_rank", "percent_rank": return - case "first_value", "last_value": + case "first_value", "last_value", "ntile": + return + case "lead", "lag": return // Mathematical functions @@ -205,14 +211,16 @@ func allowedFunction(f *sqlparser.FuncExpr) (b bool) { return case "sqrt", "pow", "power": return - case "mod", "log", "log10", "exp": + case "mod", "log", "log2", "log10", "exp": return case "sign", "ln", "truncate": return - case "sin", "cos", "tan": + case "sin", "cos", "tan", "cot": return case "asin", "acos", "atan", "atan2": return + case "conv", "degrees", "radians": + return case "rand", "pi": return @@ -235,23 +243,29 @@ func allowedFunction(f *sqlparser.FuncExpr) (b bool) { return case "ascii", "ord", "char": return - case "regexp_substr": + case "elt", "quote": + return + case "from_base64", "format": + return + case "regexp_substr", "regexp_replace", "regexp_instr", "regexp_like": return // Date functions case "str_to_date": return - case "date_format": + case "date_format", "get_format": return - case "date_add", "date_sub": + case "date_add", "adddate", "date_sub", "subdate": return - case "year", "month", "day", "weekday": + case "year", "month", "day", "weekday", "last_day": + return + case "yearweek", "weekofyear": return case "datediff": return case "unix_timestamp", "from_unixtime": return - case "extract", "hour", "minute", "second": + case "extract", "hour", "minute", "second", "microsecond": return case "dayname", "monthname", "dayofweek", "dayofmonth", "dayofyear": return @@ -259,20 +273,36 @@ func allowedFunction(f *sqlparser.FuncExpr) (b bool) { return case "timestampdiff", "timestampadd": return + case "from_days", "to_days": + return + case "time_format", "time", "timediff": + return // Type conversion case "cast", "convert": return // JSON functions - case "json_extract", "json_object", "json_array", "json_merge_patch", "json_valid": + case "json_extract", "json_object", "json_array", "json_valid": + return + case "json_merge", "json_merge_patch", "json_merge_preserve": return case "json_contains", "json_length", "json_type", "json_keys": return + case "json_contains_path", "json_depth": + return case "json_search", "json_quote", "json_unquote": return case "json_set", "json_insert", "json_replace", "json_remove": return + case "json_array_append", "json_array_insert": + return + case "json_objectagg", "json_arrayagg": + return + case "json_overlaps": + return + case "json_pretty", "json_value": + return default: return false diff --git a/pkg/expr/sql/parser_allow_test.go b/pkg/expr/sql/parser_allow_test.go index 9a36b3ad8e8..ef843fdc8fe 100644 --- a/pkg/expr/sql/parser_allow_test.go +++ b/pkg/expr/sql/parser_allow_test.go @@ -92,6 +92,16 @@ func TestAllowQuery(t *testing.T) { q: "SELECT * FROM mockGitHubIssuesDSResponse, JSON_TABLE(labels, '$[*]' COLUMNS(val VARCHAR(255) PATH '$')) AS jt WHERE CAST(jt.val AS CHAR) LIKE 'type%'", err: nil, }, + { + name: "json aggregation", + q: `SELECT JSON_ARRAYAGG(JSON_OBJECT('color', color, 'value', value)) AS result + FROM ( + SELECT 'red' AS color, 10 AS value UNION ALL + SELECT 'blue', 20 UNION ALL + SELECT 'green', 30 + ) AS t;`, + err: nil, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -215,9 +225,11 @@ SELECT STDDEV(value) AS agg_stddev, STD(value) AS agg_std, STDDEV_POP(value) AS agg_stddev_pop, + STDDEV_SAMPLE(value) AS agg_stddev_sample, VARIANCE(value) AS agg_variance, VAR_POP(value) AS agg_var_pop, - + VAR_SAMP(value) AS agg_var_samp, + -- Mathematical functions ABS(value) AS math_abs, ROUND(value, 2) AS math_round, @@ -229,6 +241,7 @@ SELECT POWER(value, 2) AS math_power, MOD(value, 10) AS math_mod, LOG(value) AS math_log, + LOG2(value) AS math_log2, LOG10(value) AS math_log10, EXP(value) AS math_exp, SIGN(value) AS math_sign, @@ -245,13 +258,25 @@ SELECT -- Date functions STR_TO_DATE('2023-01-01', '%Y-%m-%d') AS date_str_to_date, DATE_FORMAT('2025-01-01 00:00:00', '%Y-%m-%d') AS date_format, + DATE_FORMAT('2003-10-03',GET_FORMAT(DATE,'EUR')) AS date_format_eur, '2025-01-01 00:00:00' AS date_now, DATE_ADD(created_at, INTERVAL 1 DAY) AS date_add, DATE_SUB(created_at, INTERVAL 1 DAY) AS date_sub, + ADDDATE(created_at, INTERVAL 1 DAY) AS date_adddate, + SUBDATE(created_at, INTERVAL 1 DAY) AS date_subdate, YEAR(created_at) AS date_year, MONTH(created_at) AS date_month, DAY(created_at) AS date_day, + MICROSECOND(created_at) AS date_microsecond, + FROM_DAYS(738123) AS date_from_days, + TO_DAYS(created_at) AS date_to_days, + TIME(created_at) AS date_time, + TIME_FORMAT(created_at, '%H:%i:%s') AS date_time_format, + TIMEDIFF(created_at, '2025-01-01 00:00:00') AS date_timediff, WEEKDAY(created_at) AS date_weekday, + LAST_DAY(created_at) AS date_last_day, + YEARWEEK(created_at) AS date_yearweek, + WEEKOFYEAR(created_at) AS date_weekofyear, DATEDIFF('2025-01-01 00:00:00', created_at) AS date_datediff, UNIX_TIMESTAMP(created_at) AS date_unix_timestamp, FROM_UNIXTIME(1634567890) AS date_from_unixtime, @@ -266,6 +291,8 @@ LIMIT 10` var example_json_functions = `SELECT JSON_OBJECT('key1', 'value1', 'key2', 10) AS json_obj, JSON_ARRAY(1, 'abc', NULL, TRUE) AS json_arr, + JSON_ARRAY_APPEND('{"a": 1}', '$.b', 2) AS json_array_append, + JSON_ARRAY_INSERT('{"a": 1}', '$.b', 2) AS json_array_insert, JSON_EXTRACT('{"id": 123, "name": "test"}', '$.id') AS json_ext, JSON_UNQUOTE(JSON_EXTRACT('{"name": "test"}', '$.name')) AS json_unq, JSON_CONTAINS('{"a": 1, "b": 2}', '{"a": 1}') AS json_contains, @@ -273,6 +300,13 @@ var example_json_functions = `SELECT JSON_REMOVE('{"a": 1, "b": 2}', '$.b') AS json_remove, JSON_LENGTH('{"a": 1, "b": {"c": 3}}') AS json_len, JSON_SEARCH('{"a": "xyz", "b": "abc"}', 'one', 'abc') AS json_search, + JSON_MERGE('{"a": 1}', '{"b": 2}') AS json_merge, + JSON_MERGE_PRESERVE('{"a": 1}', '{"b": 2}') AS json_merge_preserve, + JSON_CONTAINS_PATH('{"a": 1, "b": 2}', 'one', '$.a') AS json_contains_path, + JSON_DEPTH('{"a": 1, "b": {"c": 2}}') AS json_depth, + JSON_OVERLAPS('{"a": 1, "b": 2}', '{"b": 2, "c": 3}') AS json_overlaps, + JSON_PRETTY('{"a": 1, "b": 2}') AS json_pretty, + JSON_VALUE('{"a": 1, "b": 2}', '$.a') AS json_value, JSON_TYPE('{"a": 1}') AS json_type` var example_many_more_allowed_functions = ` @@ -280,13 +314,17 @@ SELECT -- Math functions LN(10) as ln_val, TRUNCATE(12.345, 2) as truncate_val, + CONV('a',16,2) as conv_val, SIN(0.5) as sin_val, COS(0.5) as cos_val, + COT(0.5) as cot_val, TAN(0.5) as tan_val, ASIN(0.5) as asin_val, ACOS(0.5) as acos_val, ATAN(0.5) as atan_val, ATAN2(1, 2) as atan2_val, + DEGREES(0.5) as degrees_val, + RADIANS(0.5) as radians_val, RAND() as rand_val, PI() as pi_val, @@ -307,8 +345,19 @@ SELECT ASCII('A') as ascii_val, ORD('A') as ord_val, CHAR(65) as char_val, + ELT(2, 'one', 'two', 'three') as elt_val, + FROM_BASE64('SGVsbG8sIFdvcmxkIQ==') as from_base64_val, + FORMAT(12332.123456, 4) as format_val, + QUOTE('hello') as quote_val, + + -- Regex + 'a' REGEXP '^[a-d]' AS regexp_val, REGEXP_SUBSTR('hello world', 'world') as regexp_substr_val, - + REGEXP_REPLACE('hello world', 'world', 'gopher') as regexp_replace_val, + REGEXP_INSTR('dog cat dog', 'dog') as regexp_instr_val, + REGEXP_LIKE('Michael!', '.*') as regexp_like_val, + + -- Date functions EXTRACT(YEAR FROM '2023-01-01') as extract_val, HOUR('12:34:56') as hour_val, @@ -350,8 +399,10 @@ SELECT ROW_NUMBER() OVER (ORDER BY val) as row_num, RANK() OVER (ORDER BY val) as rank_val, DENSE_RANK() OVER (ORDER BY val) as dense_rank_val, + PERCENT_RANK() OVER (ORDER BY val) as percent_rank_val, LEAD(val) OVER (ORDER BY val) as lead_val, LAG(val) OVER (ORDER BY val) as lag_val, FIRST_VALUE(val) OVER (ORDER BY val) as first_val, + NTILE(2) OVER (ORDER BY val) as ntile_val, LAST_VALUE(val) OVER (ORDER BY val ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as last_val FROM dummy_data;` diff --git a/pkg/expr/sql_schema.go b/pkg/expr/sql_schema.go new file mode 100644 index 00000000000..c3851eed8bd --- /dev/null +++ b/pkg/expr/sql_schema.go @@ -0,0 +1,231 @@ +package expr + +import ( + "context" + "reflect" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/sql" +) + +// BasicColumn represents the column type for data that is input to a SQL expression. +type BasicColumn struct { + Name string `json:"name"` + MySQLType string `json:"mysqlType"` + Nullable bool `json:"nullable"` + DataFrameFieldType data.FieldType `json:"dataFrameFieldType"` +} + +// SchemaInfo provides information and some sample data for data that could be an input +// to a SQL expression. +type SchemaInfo struct { + Columns []BasicColumn `json:"columns"` + SampleRows [][]any `json:"sampleRows"` + Error string `json:"error,omitempty"` +} + +// SQLSchemas returns info about what the Schema for a DS query will be like if the +// query were to be used an input to SQL expressions. So effectively post SQL expressions input +// conversion. +// There is a a manual DeepCopy at the end of this file that will need to be updated when this our the +// underlying structs are change. The hack script will also need to be run to update the Query service API +// generated types. +type SQLSchemas map[string]SchemaInfo + +// GetSQLSchemas returns what the schemas are for SQL expressions for all DS queries +// in the request. It executes the queries to get the schemas. +// Intended use is for autocomplete and AI, so used during the authoring/editing experience only. +func (s *Service) GetSQLSchemas(ctx context.Context, req Request) (SQLSchemas, error) { + // Extract DS Nodes and Execute Them + // Building the pipeline is maybe not best, as it can have more errors. + filtered := make([]Query, 0, len(req.Queries)) + for _, q := range req.Queries { + if NodeTypeFromDatasourceUID(q.DataSource.UID) == TypeDatasourceNode { + filtered = append(filtered, q) + } + } + req.Queries = filtered + pipeline, err := s.buildPipeline(ctx, &req) + if err != nil { + return nil, err + } + + var schemas = make(SQLSchemas) + + for _, node := range pipeline { + // For now, execute calls convert at the end, so we are being lazy and running the full conversion. Longer run we want to run without + // full conversion and just get the schema. Maybe conversion should be + dsNode := node.(*DSNode) + // Make all input to SQL + dsNode.isInputToSQLExpr = true + + // TODO: check where time is coming from, don't recall + res, err := dsNode.Execute(ctx, time.Now(), mathexp.Vars{}, s) + if err != nil { + schemas[dsNode.RefID()] = SchemaInfo{Error: err.Error()} + continue + // we want to continue and get the schemas we can + } + if res.Error != nil { + schemas[dsNode.RefID()] = SchemaInfo{Error: res.Error.Error()} + continue + // we want to continue and get the schemas we can + } + + frames := res.Values.AsDataFrames(dsNode.RefID()) + if len(frames) == 0 { + schemas[dsNode.RefID()] = SchemaInfo{Error: "no data"} + } + frame := frames[0] + + schema := sql.SchemaFromFrame(frame) + columns := make([]BasicColumn, 0, len(schema)) + for _, col := range schema { + fT, _ := sql.MySQLColToFieldType(col) + columns = append(columns, BasicColumn{ + Name: col.Name, + MySQLType: col.Type.String(), + Nullable: col.Nullable, + DataFrameFieldType: fT, + }) + } + + // Cap at 3 rows. + const maxRows = 3 + n := frame.Rows() + if n > maxRows { + n = maxRows + } + sampleRows := make([][]any, 0, n) + for i := 0; i < n; i++ { + sampleRows = append(sampleRows, frame.RowCopy(i)) + } + + schemas[dsNode.RefID()] = SchemaInfo{Columns: columns, SampleRows: sampleRows} + } + + return schemas, nil +} + +// DeepCopy returns a deep copy of the schema. +// Used AI to make it, the kubernetes one doesn't like any or interface{} +func (s SQLSchemas) DeepCopy() SQLSchemas { + if s == nil { + return nil + } + out := make(SQLSchemas, len(s)) + for k, v := range s { + out[k] = SchemaInfo{ + Columns: copyColumns(v.Columns), + SampleRows: deepCopySampleRows2D(v.SampleRows), + Error: v.Error, + } + } + return out +} + +func copyColumns(in []BasicColumn) []BasicColumn { + if in == nil { + return nil + } + out := make([]BasicColumn, len(in)) + copy(out, in) // BasicColumn is value-only, so this suffices + return out +} + +// Deep-copy [][]any preserving nil vs empty slices and cloning elements. +func deepCopySampleRows2D(in [][]any) [][]any { + if in == nil { + return nil + } + out := make([][]any, len(in)) + for i, row := range in { + if row == nil { + // preserve nil inner slice + continue + } + newRow := make([]any, len(row)) + for j, v := range row { + newRow[j] = deepCopyAny(v) + } + out[i] = newRow + } + return out +} + +// Recursively clone pointers, maps, slices, arrays, and interfaces. +// Structs are copied by value (shallow for their internals). +func deepCopyAny(v any) any { + if v == nil { + return nil + } + return deepCopyRV(reflect.ValueOf(v)).Interface() +} + +func deepCopyRV(rv reflect.Value) reflect.Value { + if !rv.IsValid() { + return rv + } + + switch rv.Kind() { + case reflect.Ptr: + if rv.IsNil() { + return rv + } + elemCopy := deepCopyRV(rv.Elem()) + newPtr := reflect.New(rv.Type().Elem()) + if elemCopy.Type().AssignableTo(newPtr.Elem().Type()) { + newPtr.Elem().Set(elemCopy) + } else if elemCopy.Type().ConvertibleTo(newPtr.Elem().Type()) { + newPtr.Elem().Set(elemCopy.Convert(newPtr.Elem().Type())) + } else { + newPtr.Elem().Set(rv.Elem()) // fallback: shallow + } + return newPtr + + case reflect.Interface: + if rv.IsNil() { + return rv + } + return deepCopyRV(rv.Elem()) + + case reflect.Map: + if rv.IsNil() { + return reflect.Zero(rv.Type()) + } + newMap := reflect.MakeMapWithSize(rv.Type(), rv.Len()) + for _, k := range rv.MapKeys() { + newMap.SetMapIndex(deepCopyRV(k), deepCopyRV(rv.MapIndex(k))) + } + return newMap + + case reflect.Slice: + if rv.IsNil() { + return reflect.Zero(rv.Type()) + } + n := rv.Len() + newSlice := reflect.MakeSlice(rv.Type(), n, n) + for i := 0; i < n; i++ { + newSlice.Index(i).Set(deepCopyRV(rv.Index(i))) + } + return newSlice + + case reflect.Array: + n := rv.Len() + newArr := reflect.New(rv.Type()).Elem() + for i := 0; i < n; i++ { + newArr.Index(i).Set(deepCopyRV(rv.Index(i))) + } + return newArr + + case reflect.Struct: + // Value copy (OK unless the struct contains references you also want deep-copied). + return rv + + default: + // Scalars (string, bool, numbers), etc. + return rv + } +} diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index 9c052688d8f..113c2f8e4bb 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -58,4 +58,5 @@ import ( _ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1" _ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1" + _ "github.com/testcontainers/testcontainers-go" ) diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index e64b5a73234..786fabd8de8 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -191,6 +191,9 @@ type Panel struct { TimeShift *string `json:"timeShift,omitempty"` // Controls if the timeFrom or timeShift overrides are shown in the panel header HideTimeOverride *bool `json:"hideTimeOverride,omitempty"` + // Compare the current time range with a previous period + // For example "1d" to compare current period but shifted back 1 day + TimeCompare *string `json:"timeCompare,omitempty"` // Dynamically load the panel LibraryPanel *LibraryPanelRef `json:"libraryPanel,omitempty"` // Sets panel queries cache timeout. @@ -206,7 +209,8 @@ type Panel struct { // NewPanel creates a new Panel object. func NewPanel() *Panel { return &Panel{ - Transparent: (func(input bool) *bool { return &input })(false), + Transparent: (func(input bool) *bool { return &input })(false), + RepeatDirection: (func(input PanelRepeatDirection) *PanelRepeatDirection { return &input })(PanelRepeatDirectionH), } } @@ -964,6 +968,8 @@ type AnnotationQuery struct { Type *string `json:"type,omitempty"` // Set to 1 for the standard annotation query all dashboards have by default. BuiltIn *float64 `json:"builtIn,omitempty"` + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + Placement *string `json:"placement,omitempty"` } // NewAnnotationQuery creates a new AnnotationQuery object. @@ -973,6 +979,7 @@ func NewAnnotationQuery() *AnnotationQuery { Enable: true, Hide: (func(input bool) *bool { return &input })(false), BuiltIn: (func(input float64) *float64 { return &input })(0), + Placement: (func(input string) *string { return &input })(AnnotationQueryPlacement), } } @@ -1015,6 +1022,10 @@ func NewAnnotationTarget() *AnnotationTarget { } } +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +const AnnotationQueryPlacement = "inControlsMenu" + // A dashboard snapshot shares an interactive dashboard publicly. // It is a read-only version of a dashboard, and is not editable. // It is possible to create a snapshot of a snapshot. diff --git a/pkg/middleware/validate_host.go b/pkg/middleware/validate_host.go index 3b75552cd58..eb077b8e06c 100644 --- a/pkg/middleware/validate_host.go +++ b/pkg/middleware/validate_host.go @@ -3,11 +3,22 @@ package middleware import ( "strings" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) +var ( + hostRedirectCounter = promauto.NewCounter(prometheus.CounterOpts{ + Name: "host_redirect_total", + Help: "Number of requests redirected due to host header mismatch", + Namespace: "grafana", + }) +) + func ValidateHostHeader(cfg *setting.Cfg) web.Handler { return func(c *contextmodel.ReqContext) { // ignore local render calls @@ -21,6 +32,8 @@ func ValidateHostHeader(cfg *setting.Cfg) web.Handler { } if !strings.EqualFold(h, cfg.Domain) { + hostRedirectCounter.Inc() + c.Logger.Info("Enforcing Host header", "hosted", c.Req.Host, "expected", cfg.Domain) c.Redirect(strings.TrimSuffix(cfg.AppURL, "/")+c.Req.RequestURI, 301) return } diff --git a/pkg/operators/provisioning/repo_operator.go b/pkg/operators/provisioning/repo_operator.go index f96653a1d87..1651a126ffe 100644 --- a/pkg/operators/provisioning/repo_operator.go +++ b/pkg/operators/provisioning/repo_operator.go @@ -90,6 +90,7 @@ func RunRepoController(deps server.OperatorDependencies) error { statusPatcher, deps.Registerer, tracer, + controllerCfg.parallelOperations, ) if err != nil { return fmt.Errorf("failed to create repository controller: %w", err) @@ -107,6 +108,7 @@ func RunRepoController(deps server.OperatorDependencies) error { type repoControllerConfig struct { provisioningControllerConfig workerCount int + parallelOperations int allowedTargets []string allowImageRendering bool minSyncInterval time.Duration @@ -128,6 +130,7 @@ func getRepoControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) ( provisioningControllerConfig: *controllerCfg, allowedTargets: allowedTargets, workerCount: cfg.SectionWithEnvOverrides("operator").Key("worker_count").MustInt(1), + parallelOperations: cfg.SectionWithEnvOverrides("operator").Key("parallel_operations").MustInt(10), allowImageRendering: cfg.SectionWithEnvOverrides("provisioning").Key("allow_image_rendering").MustBool(false), minSyncInterval: cfg.SectionWithEnvOverrides("provisioning").Key("min_sync_interval").MustDuration(1 * time.Minute), }, nil diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index b650d20f0e4..08295065017 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -7,7 +7,7 @@ replace github.com/grafana/grafana/pkg/codegen => ../../codegen require ( cuelang.org/go v0.11.1 github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d - github.com/grafana/cog v0.0.43 + github.com/grafana/cog v0.0.44 github.com/grafana/cuetsy v0.1.11 github.com/grafana/grafana/pkg/codegen v0.0.0-20250514132646-acbc7b54ed9e ) @@ -43,11 +43,11 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect github.com/yalue/merged_fs v1.3.0 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/tools v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index b4b3bda5f8f..0e065f7c4cd 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -30,8 +30,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cog v0.0.43 h1:6EDzJVc8hbP3+AjPnRnAK6mKfyWgqLKbi/jmiStilFk= -github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= +github.com/grafana/cog v0.0.44 h1:N8UP7g6XBHZXf1wY7AOOWC2HdqlTPBJPJiAZQrkf4XQ= +github.com/grafana/cog v0.0.44/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk= github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -94,20 +94,20 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY= github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go index e67b04b69f2..98840189389 100644 --- a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go @@ -35,6 +35,11 @@ func NewPatternListInspector(detectorsProvider angulardetector.DetectorsProvider } func (i *PatternsListInspector) Inspect(ctx context.Context, p *plugins.Plugin) (isAngular bool, err error) { + // CDN plugins are ignored because they should not be using Angular + if p.Class == plugins.ClassCDN { + return false, nil + } + f, err := p.FS.Open("module.js") if err != nil { if errors.Is(err, plugins.ErrFileNotExist) { diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go index f45ae982d09..47c5af990b9 100644 --- a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go @@ -27,17 +27,17 @@ func (d *fakeDetector) String() string { } func TestPatternsListInspector(t *testing.T) { - plugin := &plugins.Plugin{ - FS: plugins.NewInMemoryFS(map[string][]byte{"module.js": nil}), - } - for _, tc := range []struct { name string + plugin *plugins.Plugin fakeDetectors []*fakeDetector exp func(t *testing.T, r bool, err error, fakeDetectors []*fakeDetector) }{ { name: "calls the detectors in sequence until true is returned", + plugin: &plugins.Plugin{ + FS: plugins.NewInMemoryFS(map[string][]byte{"module.js": nil}), + }, fakeDetectors: []*fakeDetector{ {returns: false}, {returns: true}, @@ -53,6 +53,9 @@ func TestPatternsListInspector(t *testing.T) { }, { name: "calls the detectors in sequence and returns false as default", + plugin: &plugins.Plugin{ + FS: plugins.NewInMemoryFS(map[string][]byte{"module.js": nil}), + }, fakeDetectors: []*fakeDetector{ {returns: false}, {returns: false}, @@ -65,13 +68,30 @@ func TestPatternsListInspector(t *testing.T) { }, }, { - name: "empty detectors should return false", + name: "empty detectors should return false", + plugin: &plugins.Plugin{ + FS: plugins.NewInMemoryFS(map[string][]byte{"module.js": nil}), + }, fakeDetectors: nil, exp: func(t *testing.T, r bool, err error, fakeDetectors []*fakeDetector) { require.NoError(t, err) require.False(t, r, "inspector should return false") }, }, + { + name: "CDN plugins return false without calling detectors", + plugin: &plugins.Plugin{ + Class: plugins.ClassCDN, + }, + fakeDetectors: []*fakeDetector{ + {returns: true}, + }, + exp: func(t *testing.T, r bool, err error, fakeDetectors []*fakeDetector) { + require.NoError(t, err) + require.False(t, r, "inspector should return false for CDN plugins") + require.Equal(t, 0, fakeDetectors[0].calls, "detectors should not be called for CDN plugins") + }, + }, } { t.Run(tc.name, func(t *testing.T) { detectors := make([]angulardetector.AngularDetector, 0, len(tc.fakeDetectors)) @@ -79,7 +99,7 @@ func TestPatternsListInspector(t *testing.T) { detectors = append(detectors, angulardetector.AngularDetector(d)) } inspector := NewPatternListInspector(&angulardetector.StaticDetectorsProvider{Detectors: detectors}) - r, err := inspector.Inspect(context.Background(), plugin) + r, err := inspector.Inspect(context.Background(), tc.plugin) tc.exp(t, r, err, tc.fakeDetectors) }) } diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 52911b5ae22..c545a9b1e8a 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -18,6 +18,7 @@ require ( ) require ( + cloud.google.com/go/auth v0.16.3 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect github.com/apache/arrow-go/v18 v18.4.1 // indirect @@ -47,6 +48,7 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect @@ -97,16 +99,16 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.13.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/api v0.235.0 // indirect + google.golang.org/api v0.242.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 8166d4a39bd..01986d4d1c7 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= -cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= -cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= +cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= @@ -126,8 +126,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/grafana-plugin-sdk-go v0.281.0 h1:V8dGyatzcOLQeivFhBV2JWMwTSZH/clDnpfKG9p3dTA= @@ -319,20 +319,20 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -352,20 +352,20 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -374,8 +374,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= -google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= +google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 98a09583c85..a1d323f996a 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -30,6 +30,7 @@ func ProvideRegistryServiceSink( _ *provisioning.APIBuilder, _ *ofrep.APIBuilder, _ *secret.DependencyRegisterer, + _ *provisioning.DependencyRegisterer, ) *Service { return &Service{} } diff --git a/pkg/registry/apis/dashboard/datasources.go b/pkg/registry/apis/dashboard/datasources.go index 5de714652da..891938009f9 100644 --- a/pkg/registry/apis/dashboard/datasources.go +++ b/pkg/registry/apis/dashboard/datasources.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/datasources" ) @@ -11,9 +12,20 @@ type datasourceInfoProvider struct { datasourceService datasources.DataSourceService } -func (d *datasourceInfoProvider) GetDataSourceInfo(_ context.Context) []schemaversion.DataSourceInfo { - query := datasources.GetAllDataSourcesQuery{} - dataSources, err := d.datasourceService.GetAllDataSources(context.Background(), &query) +func (d *datasourceInfoProvider) GetDataSourceInfo(ctx context.Context) []schemaversion.DataSourceInfo { + // Extract namespace info from context to get OrgID + nsInfo, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + // If namespace info is not available, return empty list + return []schemaversion.DataSourceInfo{} + } + + // Use GetDataSources with OrgID query instead of GetAllDataSources + // This ensures tenant-aware datasource retrieval + query := datasources.GetDataSourcesQuery{ + OrgID: nsInfo.OrgID, + } + dataSources, err := d.datasourceService.GetDataSources(ctx, &query) if err != nil { return []schemaversion.DataSourceInfo{} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 5676566fd1a..0fca282fa82 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "go.opentelemetry.io/otel" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -40,7 +41,8 @@ import ( ) var ( - _ DashboardAccess = (*dashboardSqlAccess)(nil) + _ DashboardAccess = (*dashboardSqlAccess)(nil) + tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy") ) type dashboardRow struct { @@ -105,6 +107,9 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, } func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyDatabaseHelper, query *DashboardQuery) (*rowsWrapper, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.getRows") + defer span.End() + if len(query.Labels) > 0 { return nil, fmt.Errorf("labels not yet supported") // if query.Requirements.Folder != nil { @@ -416,6 +421,9 @@ func getUserID(v sql.NullString, id sql.NullInt64) string { // DeleteDashboard implements DashboardAccess. func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardV1.Dashboard, bool, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.DeleteDashboard") + defer span.End() + dash, _, err := a.GetDashboard(ctx, orgId, uid, 0) if err != nil { return nil, false, err @@ -432,6 +440,9 @@ func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, u } func (a *dashboardSqlAccess) buildSaveDashboardCommand(ctx context.Context, orgId int64, dash *dashboardV1.Dashboard) (*dashboards.SaveDashboardCommand, bool, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.buildSaveDashboardCommand") + defer span.End() + created := false user, ok := claims.AuthInfoFrom(ctx) if !ok || user == nil { @@ -495,6 +506,9 @@ func (a *dashboardSqlAccess) buildSaveDashboardCommand(ctx context.Context, orgI } func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, dash *dashboardV1.Dashboard, failOnExisting bool) (*dashboardV1.Dashboard, bool, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.SaveDashboard") + defer span.End() + user, ok := claims.AuthInfoFrom(ctx) if !ok || user == nil { return nil, false, fmt.Errorf("no user found in context") @@ -565,6 +579,9 @@ type panel struct { } func (a *dashboardSqlAccess) GetLibraryPanels(ctx context.Context, query LibraryPanelQuery) (*dashboardV0.LibraryPanelList, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.GetLibraryPanels") + defer span.End() + limit := int(query.Limit) query.Limit += 1 // for continue if query.OrgID == 0 { diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index d5947ab24f5..d8c948c9664 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -78,6 +78,9 @@ func isDashboardKey(key *resourcepb.ResourceKey, requireName bool) error { } func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.WriteEvent) (rv int64, err error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.WriteEvent") + defer span.End() + info, err := claims.ParseNamespace(event.Key.Namespace) if err == nil { err = isDashboardKey(event.Key, true) @@ -170,6 +173,9 @@ func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.Writ } func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid string, v int64) (*dashboard.Dashboard, int64, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.GetDashboard") + defer span.End() + sql, err := a.sql(ctx) if err != nil { return nil, 0, err @@ -197,6 +203,9 @@ func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid // Read implements ResourceStoreServer. func (a *dashboardSqlAccess) ReadResource(ctx context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.ReadResource") + defer span.End() + rsp := &resource.BackendReadResponse{} info, err := claims.ParseNamespace(req.Key.Namespace) if err == nil { @@ -238,16 +247,25 @@ func (a *dashboardSqlAccess) ReadResource(ctx context.Context, req *resourcepb.R // ListHistory implements StorageBackend. func (a *dashboardSqlAccess) ListHistory(ctx context.Context, req *resourcepb.ListRequest, cb func(resource.ListIterator) error) (int64, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.ListHistory") + defer span.End() + return a.ListIterator(ctx, req, cb) } func (a *dashboardSqlAccess) ListModifiedSince(ctx context.Context, key resource.NamespacedResource, sinceRv int64) (int64, iter.Seq2[*resource.ModifiedResource, error]) { + _, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.ListModifiedSince") + defer span.End() + return 0, func(yield func(*resource.ModifiedResource, error) bool) { yield(nil, errors.New("not implemented")) } } func (a *dashboardSqlAccess) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[resource.ResourceLastImportTime, error] { + _, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.GetResourceLastImportTimes") + defer span.End() + return func(yield func(resource.ResourceLastImportTime, error) bool) { yield(resource.ResourceLastImportTime{}, errors.New("not implemented")) } @@ -255,6 +273,9 @@ func (a *dashboardSqlAccess) GetResourceLastImportTimes(ctx context.Context) ite // List implements StorageBackend. func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resourcepb.ListRequest, cb func(resource.ListIterator) error) (int64, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.ListIterator") + defer span.End() + if req.ResourceVersion != 0 { return 0, apierrors.NewBadRequest("List with explicit resourceVersion is not supported with this storage backend") } @@ -348,10 +369,16 @@ func (a *dashboardSqlAccess) WatchWriteEvents(ctx context.Context) (<-chan *reso // Simple wrapper for index implementation func (a *dashboardSqlAccess) Read(ctx context.Context, req *resourcepb.ReadRequest) (*resource.BackendReadResponse, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.Read") + defer span.End() + return a.ReadResource(ctx, req), nil } func (a *dashboardSqlAccess) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest) (*resourcepb.ResourceSearchResponse, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.Search") + defer span.End() + return a.dashboardSearchClient.Search(ctx, req) } @@ -365,5 +392,8 @@ func (a *dashboardSqlAccess) CountManagedObjects(context.Context, *resourcepb.Co // GetStats implements ResourceServer. func (a *dashboardSqlAccess) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest) (*resourcepb.ResourceStatsResponse, error) { + ctx, span := tracer.Start(ctx, "legacy.dashboardSqlAccess.GetStats") + defer span.End() + return a.dashboardSearchClient.GetStats(ctx, req) } diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index 446e7e07373..dde82737e23 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -8,6 +8,7 @@ import ( "strings" "google.golang.org/grpc" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/selection" claims "github.com/grafana/authlib/types" @@ -62,7 +63,7 @@ func ParseSortName(sortName string) (string, bool, error) { } } - return "", false, fmt.Errorf("no matching sort field found for: %s", sortName) + return "", false, apierrors.NewBadRequest(fmt.Sprintf("no matching sort field found for: %s", sortName)) } // nolint:gocyclo @@ -97,11 +98,11 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso case folders.RESOURCE: queryType = searchstore.TypeFolder default: - return nil, fmt.Errorf("bad type request") + return nil, apierrors.NewBadRequest("bad type request") } if len(req.Federated) > 1 { - return nil, fmt.Errorf("bad type request") + return nil, apierrors.NewBadRequest("bad type request") } if len(req.Federated) == 1 && @@ -117,7 +118,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso sortByField := "" if len(req.SortBy) != 0 { if len(req.SortBy) > 1 { - return nil, fmt.Errorf("only one sort field is supported") + return nil, apierrors.NewBadRequest("only one sort field is supported") } sort := req.SortBy[0] sortByField = strings.TrimPrefix(sort.Field, resource.SEARCH_FIELD_PREFIX) @@ -208,13 +209,13 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso case resource.SEARCH_FIELD_SOURCE_PATH: // only one value is supported in legacy search if len(vals) != 1 { - return nil, fmt.Errorf("only one repo path query is supported") + return nil, apierrors.NewBadRequest("only one repo path query is supported") } query.SourcePath = vals[0] case resource.SEARCH_FIELD_MANAGER_KIND: if len(vals) != 1 { - return nil, fmt.Errorf("only one manager kind supported") + return nil, apierrors.NewBadRequest("only one manager kind supported") } query.ManagedBy = utils.ManagerKind(vals[0]) @@ -226,18 +227,38 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso // only one value is supported in legacy search if len(vals) != 1 { - return nil, fmt.Errorf("only one repo name is supported") + return nil, apierrors.NewBadRequest("only one repo name is supported") } query.ManagerIdentity = vals[0] + case unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE: if len(vals) != 1 { - return nil, fmt.Errorf("only one library panel uid is supported") + return nil, apierrors.NewBadRequest("only one library panel uid is supported") + } + + // Make sure the query does not include incompatible combinations + for _, f := range req.Options.Fields { + switch f.Key { + case resource.SEARCH_FIELD_NAME: + return nil, apierrors.NewBadRequest("libraryPanel query must not include explicit names") + } + } + + query.DashboardUIDs, err = c.getLibraryPanelConnections(ctx, user, vals[0]) + if err != nil { + return nil, err + } + if len(query.DashboardUIDs) == 0 { + // Empty results + return &resourcepb.ResourceSearchResponse{ + TotalHits: 0, + Results: &resourcepb.ResourceTable{}, + }, nil } - return c.getLibraryPanelConnections(ctx, user, vals[0], req.Options.Key.Namespace) case resource.SEARCH_FIELD_TITLE_PHRASE: if len(vals) != 1 { - return nil, fmt.Errorf("only one title supported") + return nil, apierrors.NewBadRequest("only one title supported") } query.Title = vals[0] @@ -256,7 +277,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso // legacy sql query, since legacy search does not support this if query.ManagerIdentity != "" || len(query.ManagerIdentityNotIn) > 0 || query.ManagedBy != "" { if query.ManagedBy == utils.ManagerKindUnknown { - return nil, fmt.Errorf("query by manager identity also requires manager.kind parameter") + return nil, apierrors.NewBadRequest("query by manager identity also requires manager.kind parameter") } // for plugin and orphaned dashboards, we will only return the manager kind alongside the regular search response @@ -312,7 +333,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso for _, dashboard := range provisioningData { list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{ Key: getResourceKey(&dashboards.DashboardSearchProjection{ - UID: dashboard.Dashboard.UID, + UID: dashboard.UID, }, req.Options.Key.Namespace), Cells: c.createDetailedProvisioningCells(dashboard, query), }) @@ -362,50 +383,36 @@ func getResourceKey(item *dashboards.DashboardSearchProjection, namespace string } } -// retrieves all the dashboards that are connected to the given library panel -func (c *DashboardSearchClient) getLibraryPanelConnections(ctx context.Context, user identity.Requester, libraryElementUID, namespace string) (*resourcepb.ResourceSearchResponse, error) { +// retrieves all dashboard UIDs connected to a given library panel +func (c *DashboardSearchClient) getLibraryPanelConnections(ctx context.Context, user identity.Requester, libraryElementUID string) ([]string, error) { connections, err := c.dashboardStore.GetDashboardsByLibraryPanelUID(ctx, libraryElementUID, user.GetOrgID()) if err != nil { return nil, err } - columns := c.getColumns("", &dashboards.FindPersistedDashboardsQuery{}) - list := &resourcepb.ResourceSearchResponse{ - Results: &resourcepb.ResourceTable{ - Columns: columns, - }, + uids := make([]string, len(connections)) + for i, dashboard := range connections { + uids[i] = dashboard.UID } - - for _, dashboard := range connections { - cells := c.createCommonCells("", dashboard.FolderUID, dashboard.ID, nil) // nolint:staticcheck - list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{ - Key: getResourceKey(&dashboards.DashboardSearchProjection{ - UID: dashboard.UID, - }, namespace), - Cells: cells, - }) - } - - list.TotalHits = int64(len(list.Results.Rows)) - return list, nil + return uids, nil } func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest, _ ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) { info, err := claims.ParseNamespace(req.Namespace) if err != nil { - return nil, fmt.Errorf("unable to read namespace") + return nil, apierrors.NewInternalError(fmt.Errorf("unable to read namespace: %w", err)) } if info.OrgID == 0 { - return nil, fmt.Errorf("invalid OrgID found in namespace") + return nil, apierrors.NewInternalError(fmt.Errorf("invalid OrgID found in namespace")) } if len(req.Kinds) != 1 { - return nil, fmt.Errorf("only can query for dashboard kind in legacy fallback") + return nil, apierrors.NewBadRequest("only can query for dashboard kind in legacy fallback") } parts := strings.SplitN(req.Kinds[0], "/", 2) if len(parts) != 2 { - return nil, fmt.Errorf("invalid kind") + return nil, apierrors.NewBadRequest("invalid kind") } var count int64 @@ -415,7 +422,7 @@ func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resourcepb.Re case folders.GROUP: count, err = c.dashboardStore.CountInOrg(ctx, info.OrgID, true) default: - return nil, fmt.Errorf("invalid group") + return nil, apierrors.NewBadRequest("invalid group") } if err != nil { return nil, err @@ -512,7 +519,7 @@ func (c *DashboardSearchClient) createProvisioningCells(dashboard *dashboards.Da } func (c *DashboardSearchClient) createDetailedProvisioningCells(dashboard *dashboards.DashboardProvisioningSearchResults, query *dashboards.FindPersistedDashboardsQuery) [][]byte { - cells := c.createCommonCells(dashboard.Dashboard.Title, dashboard.Dashboard.FolderUID, dashboard.Dashboard.ID, []byte("[]")) + cells := c.createCommonCells(dashboard.Title, dashboard.FolderUID, dashboard.ID, []byte("[]")) return append(cells, []byte(query.ManagedBy), []byte(dashboard.Provisioner), diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go index 94fd1accff3..8f1974bee68 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go @@ -454,7 +454,9 @@ func TestDashboardSearchClient_Search(t *testing.T) { t.Run("Should retrieve dashboards by provisioner name through a different function", func(t *testing.T) { mockStore.On("GetProvisionedDashboardsByName", mock.Anything, "test", mock.Anything).Return([]*dashboards.DashboardProvisioningSearchResults{ { - Dashboard: dashboards.Dashboard{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"}, + UID: "uid", + Title: "Test Dashboard", + FolderUID: "folder1", ExternalID: "test", Provisioner: string(utils.ManagerKindClassicFP), // nolint:staticcheck }, @@ -579,6 +581,11 @@ func TestDashboardSearchClient_Search(t *testing.T) { {UID: "dashboard2", FolderUID: "folder2", ID: 2}, }, nil).Once() + mockStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ + {UID: "dashboard1", FolderUID: "folder1", ID: 1}, + {UID: "dashboard2", FolderUID: "folder2", ID: 2}, + }, nil).Once() + req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ Key: dashboardKey, @@ -638,6 +645,54 @@ func TestDashboardSearchClient_Search(t *testing.T) { require.Contains(t, err.Error(), "only one library panel uid is supported") require.Nil(t, resp) }) + + t.Run("Should reject library panel query when combined with explicit dashboard names", func(t *testing.T) { + req := &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + { + Key: unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Operator: "=", + Values: []string{"test-library-panel"}, + }, + { + Key: resource.SEARCH_FIELD_NAME, + Operator: "=", + Values: []string{"dashboard-uid"}, + }, + }, + }, + } + resp, err := client.Search(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "libraryPanel query must not include explicit names") + require.Nil(t, resp) + // Note: mockStore should NOT be called since validation happens before any store calls + }) + + t.Run("Should return empty results when library panel has no connected dashboards", func(t *testing.T) { + mockStore.On("GetDashboardsByLibraryPanelUID", mock.Anything, "unused-library-panel", int64(2)).Return([]*dashboards.DashboardRef{}, nil).Once() + + req := &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + { + Key: unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Operator: "=", + Values: []string{"unused-library-panel"}, + }, + }, + }, + } + resp, err := client.Search(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, int64(0), resp.TotalHits) + require.Empty(t, resp.Results.Rows) + mockStore.AssertExpectations(t) + }) } func TestParseSortName(t *testing.T) { diff --git a/pkg/registry/apis/dashboard/libary_panel.go b/pkg/registry/apis/dashboard/libary_panel.go index c22594ff590..aa2b2705bb1 100644 --- a/pkg/registry/apis/dashboard/libary_panel.go +++ b/pkg/registry/apis/dashboard/libary_panel.go @@ -5,16 +5,17 @@ import ( "fmt" "strconv" + 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" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - apierrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/grafana/grafana/pkg/services/libraryelements" ) var ( @@ -29,9 +30,9 @@ var ( ) type LibraryPanelStore struct { - Access legacy.DashboardAccess - ResourceInfo utils.ResourceInfo - AccessControl accesscontrol.AccessControl + Access legacy.DashboardAccess + ResourceInfo utils.ResourceInfo + service libraryelements.Service } func (s *LibraryPanelStore) New() runtime.Object { @@ -57,15 +58,70 @@ func (s *LibraryPanelStore) ConvertToTable(ctx context.Context, object runtime.O } func (s *LibraryPanelStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - return nil, fmt.Errorf("method not yet implemented") + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + cmd, err := libraryelements.ToCreateLibraryElementCommand(obj) + if err != nil { + return nil, err + } + + // NOTE: this includes all access control checks + out, err := s.service.CreateElement(ctx, user, *cmd) + if err != nil { + return nil, err + } + if out.UID == "" { + return nil, fmt.Errorf("created library panel has empty UID") + } + return s.Get(ctx, out.UID, &metav1.GetOptions{}) } func (s *LibraryPanelStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - return nil, false, fmt.Errorf("method not yet implemented") + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, false, err + } + old, err := s.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + + // NOTE: this includes all access control checks + obj, err := objInfo.UpdatedObject(ctx, old) + if err != nil { + return nil, false, err + } + + cmd, err := libraryelements.ToPatchLibraryElementCommand(obj) + if err != nil { + return nil, false, err + } + + out, err := s.service.PatchLibraryElement(ctx, user, *cmd, name) + if err != nil { + return nil, false, err + } + if out.UID == "" { + return nil, false, fmt.Errorf("created library panel has empty UID") + } + obj, err = s.Get(ctx, out.UID, &metav1.GetOptions{}) + return obj, false, err } func (s *LibraryPanelStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - return nil, false, fmt.Errorf("method not yet implemented") + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, false, err + } + + // NOTE: this includes all access control checks + _, err = s.service.DeleteLibraryElement(ctx, user, name) + if err != nil { + return nil, false, err + } + return nil, true, nil } func (s *LibraryPanelStore) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { diff --git a/pkg/registry/apis/dashboard/mutate.go b/pkg/registry/apis/dashboard/mutate.go index 3703e14c796..c193c17cf56 100644 --- a/pkg/registry/apis/dashboard/mutate.go +++ b/pkg/registry/apis/dashboard/mutate.go @@ -26,6 +26,19 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute if op != admission.Create && op != admission.Update { return nil } + + switch a.GetResource().Resource { + case dashboardV0.DASHBOARD_RESOURCE: + return b.mutateDashboard(ctx, a) + + case dashboardV0.LIBRARY_PANEL_RESOURCE: + return nil // nothing needed + } + + return fmt.Errorf("unexpected resource: %+v", a.GetResource()) +} + +func (b *DashboardsAPIBuilder) mutateDashboard(ctx context.Context, a admission.Attributes) (err error) { var internalID int64 obj := a.GetObject() meta, err := utils.MetaAccessor(obj) diff --git a/pkg/registry/apis/dashboard/mutation_test.go b/pkg/registry/apis/dashboard/mutation_test.go index 4c940160716..ad34c3b3218 100644 --- a/pkg/registry/apis/dashboard/mutation_test.go +++ b/pkg/registry/apis/dashboard/mutation_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" @@ -180,10 +179,10 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { err := b.Mutate(context.Background(), admission.NewAttributesRecord( tt.inputObj, nil, - schema.GroupVersionKind{}, + dashv1.DashboardResourceInfo.GroupVersionKind(), "", "test", - schema.GroupVersionResource{}, + dashv1.DashboardResourceInfo.GroupVersionResource(), "", tt.operation, operationOptions, diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index dadc2c079b4..20629866d6f 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -50,8 +50,10 @@ import ( dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/provisioning" + "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/user" @@ -68,6 +70,8 @@ var ( _ builder.APIGroupVersionsProvider = (*DashboardsAPIBuilder)(nil) _ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil) _ builder.APIGroupRouteProvider = (*DashboardsAPIBuilder)(nil) + _ builder.APIGroupMutation = (*DashboardsAPIBuilder)(nil) + _ builder.APIGroupValidation = (*DashboardsAPIBuilder)(nil) ) const ( @@ -108,6 +112,8 @@ type DashboardsAPIBuilder struct { minRefreshInterval string dualWriter dualwrite.Service folderClientProvider client.K8sHandlerProvider + libraryPanels libraryelements.Service // for legacy library panels + publicDashboardService publicdashboards.Service isStandalone bool // skips any handling including anything to do with legacy storage } @@ -135,6 +141,8 @@ func RegisterAPIService( libraryPanelSvc librarypanels.Service, restConfigProvider apiserver.RestConfigProvider, userService user.Service, + libraryPanels libraryelements.Service, + publicDashboardService publicdashboards.Service, ) *DashboardsAPIBuilder { dbp := legacysql.NewDatabaseProvider(sql) namespacer := request.GetNamespaceMapper(cfg) @@ -157,6 +165,8 @@ func RegisterAPIService( minRefreshInterval: cfg.MinRefreshInterval, dualWriter: dual, folderClientProvider: newSimpleFolderClientProvider(folderClient), + libraryPanels: libraryPanels, + publicDashboardService: publicDashboardService, legacy: &DashboardStorage{ Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features), @@ -221,7 +231,7 @@ func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { } // Register the explicit conversions - if err := conversion.RegisterConversions(scheme); err != nil { + if err := conversion.RegisterConversions(scheme, migration.GetDataSourceInfoProvider()); err != nil { return err } @@ -229,26 +239,35 @@ func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { } func (b *DashboardsAPIBuilder) AllowedV0Alpha1Resources() []string { - return []string{dashv0.DashboardKind().Plural()} + return []string{ + dashv0.DashboardKind().Plural(), + dashv0.LIBRARY_PANEL_RESOURCE, + } } // Validate validates dashboard operations for the apiserver func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { op := a.GetOperation() - // Handle different operations - switch op { - case admission.Delete: - return b.validateDelete(ctx, a) - case admission.Create: - return b.validateCreate(ctx, a, o) - case admission.Update: - return b.validateUpdate(ctx, a, o) - case admission.Connect: - return nil + switch a.GetResource().Resource { + case dashv0.DASHBOARD_RESOURCE: + // Handle different operations + switch op { + case admission.Delete: + return b.validateDelete(ctx, a) + case admission.Create: + return b.validateCreate(ctx, a, o) + case admission.Update: + return b.validateUpdate(ctx, a, o) + case admission.Connect: + return nil + } + + case dashv0.LIBRARY_PANEL_RESOURCE: + return nil // OK for now } - return nil + return fmt.Errorf("unsupported validation: %+v", a.GetResource()) } // validateDelete checks if a dashboard can be deleted @@ -637,17 +656,19 @@ func (b *DashboardsAPIBuilder) storageForVersion( b.accessControl, opts.Scheme, newDTOFunc, + b.publicDashboardService, ) if err != nil { return err } - // Expose read only library panels - if libraryPanels != nil { + // Expose read library panels + //nolint:staticcheck // not yet migrated to OpenFeature + if libraryPanels != nil && b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { legacyLibraryStore := &LibraryPanelStore{ - Access: b.legacy.Access, - ResourceInfo: *libraryPanels, - AccessControl: b.accessControl, + Access: b.legacy.Access, + ResourceInfo: *libraryPanels, + service: b.libraryPanels, } unifiedLibraryStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, *libraryPanels, opts.OptsGetter) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 7dba6e1f8f1..d471cba752a 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -121,6 +121,15 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) * Schema: spec.ArrayProperty(spec.StringProperty()), }, }, + { + ParameterProps: spec3.ParameterProps{ + Name: "libraryPanel", + In: "query", + Description: "find dashboards that reference a given libraryPanel", + Required: false, + Schema: spec.StringProperty(), + }, + }, { ParameterProps: spec3.ParameterProps{ Name: "sort", @@ -363,6 +372,15 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { }} } + // The libraryPanel filter + if libraryPanel, ok := queryParams["libraryPanel"]; ok { + searchRequest.Options.Fields = []*resourcepb.Requirement{{ + Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Operator: "=", + Values: libraryPanel, + }} + } + // The names filter names := queryParams["name"] diff --git a/pkg/registry/apis/dashboard/sub_dto.go b/pkg/registry/apis/dashboard/sub_dto.go index 30e58e0d22e..f2e9bc0b6df 100644 --- a/pkg/registry/apis/dashboard/sub_dto.go +++ b/pkg/registry/apis/dashboard/sub_dto.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" @@ -28,13 +29,14 @@ type dtoBuilder = func(dashboard runtime.Object, access *dashboard.DashboardAcce // The DTO returns everything the UI needs in a single request type DTOConnector struct { - getter rest.Getter - legacy legacy.DashboardAccess - unified resource.ResourceClient - largeObjects apistore.LargeObjectSupport - accessControl accesscontrol.AccessControl - scheme *runtime.Scheme - builder dtoBuilder + getter rest.Getter + legacy legacy.DashboardAccess + unified resource.ResourceClient + largeObjects apistore.LargeObjectSupport + accessControl accesscontrol.AccessControl + scheme *runtime.Scheme + builder dtoBuilder + publicDashboardService publicdashboards.Service } func NewDTOConnector( @@ -45,15 +47,17 @@ func NewDTOConnector( accessControl accesscontrol.AccessControl, scheme *runtime.Scheme, builder dtoBuilder, + publicDashboardService publicdashboards.Service, ) (rest.Storage, error) { return &DTOConnector{ - getter: getter, - legacy: legacyAccess, - accessControl: accessControl, - unified: resourceClient, - largeObjects: largeObjects, - builder: builder, - scheme: scheme, + getter: getter, + legacy: legacyAccess, + accessControl: accessControl, + unified: resourceClient, + largeObjects: largeObjects, + builder: builder, + scheme: scheme, + publicDashboardService: publicDashboardService, }, nil } @@ -154,6 +158,11 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob access.Slug = slugify.Slugify(title) access.Url = dashboards.GetDashboardFolderURL(false, name, access.Slug) + pubDash, err := r.publicDashboardService.FindByDashboardUid(ctx, user.GetOrgID(), name) + if err == nil && pubDash != nil { + access.IsPublic = true + } + dash, err := r.builder(rawobj, access) if err != nil { responder.Error(err) diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index 90b1ad8a52f..9feb46a33d4 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -3,10 +3,8 @@ package datasource import ( "context" "encoding/json" - "errors" "fmt" "maps" - "path/filepath" "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,14 +14,12 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" openapi "k8s.io/kube-openapi/pkg/common" - "k8s.io/utils/strings/slices" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/apimachinery/utils" datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - "github.com/grafana/grafana/pkg/configprovider" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/promlib/models" @@ -31,7 +27,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds" ) @@ -53,7 +48,6 @@ type DataSourceAPIBuilder struct { } func RegisterAPIService( - cfgProvider configprovider.ConfigProvider, features featuremgmt.FeatureToggles, apiRegistrar builder.APIRegistrar, pluginClient plugins.Client, // access to everything @@ -61,54 +55,29 @@ func RegisterAPIService( contextProvider PluginContextWrapper, accessControl accesscontrol.AccessControl, reg prometheus.Registerer, + pluginSources sources.Registry, ) (*DataSourceAPIBuilder, error) { - // We want to expose just a limited set of plugins //nolint:staticcheck // not yet migrated to OpenFeature - explicitPluginList := features.IsEnabledGlobally(featuremgmt.FlagDatasourceAPIServers) - - // This requires devmode! - //nolint:staticcheck // not yet migrated to OpenFeature - if !explicitPluginList && !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil, nil // skip registration unless opting into experimental apis + if !features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections) && !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + return nil, nil } var err error var builder *DataSourceAPIBuilder - cfg, err := cfgProvider.Get(context.Background()) + pluginJSONs, err := getDatasourcePlugins(pluginSources) if err != nil { - return nil, err - } - pluginJSONs, err := getCorePlugins(cfg) - if err != nil { - return nil, err - } - - ids := []string{ - "grafana-testdata-datasource", - "prometheus", - "graphite", + return nil, fmt.Errorf("error getting list of datasource plugins: %s", err) } for _, pluginJSON := range pluginJSONs { - if explicitPluginList && !slices.Contains(ids, pluginJSON.ID) { - continue // skip this one - } - - if !pluginJSON.Backend { - continue // skip frontend only plugins - } - - if pluginJSON.Type != plugins.TypeDataSource { - continue // skip non-datasource plugins - } - client, ok := pluginClient.(PluginClient) if !ok { return nil, fmt.Errorf("plugin client is not a PluginClient: %T", pluginClient) } - builder, err = NewDataSourceAPIBuilder(pluginJSON, + builder, err = NewDataSourceAPIBuilder( + pluginJSON, client, datasources.GetDatasourceProvider(pluginJSON), contextProvider, @@ -299,21 +268,32 @@ func (b *DataSourceAPIBuilder) GetOpenAPIDefinitions() openapi.GetOpenAPIDefinit } } -func getCorePlugins(cfg *setting.Cfg) ([]plugins.JSONData, error) { - coreDataSourcesPath := filepath.Join(cfg.StaticRootPath, "app", "plugins", "datasource") - coreDataSourcesSrc := sources.NewLocalSource( - plugins.ClassCore, - []string{coreDataSourcesPath}, - ) +func getDatasourcePlugins(pluginSources sources.Registry) ([]plugins.JSONData, error) { + var pluginJSONs []plugins.JSONData - res, err := coreDataSourcesSrc.Discover(context.Background()) - if err != nil { - return nil, errors.New("failed to load core data source plugins") - } + // It's possible that the same plugin will be found in different sources. + // Registering the same plugin twice in the API is Probably A Bad Thing, + // so this map keeps track of uniques, so we can skip duplicates. + var uniquePlugins = map[string]bool{} - pluginJSONs := make([]plugins.JSONData, 0, len(res)) - for _, p := range res { - pluginJSONs = append(pluginJSONs, p.Primary.JSONData) + for _, pluginSource := range pluginSources.List(context.Background()) { + res, err := pluginSource.Discover(context.Background()) + if err != nil { + return nil, err + } + for _, p := range res { + if !p.Primary.JSONData.Backend || p.Primary.JSONData.Type != plugins.TypeDataSource { + continue + } + + if _, found := uniquePlugins[p.Primary.JSONData.ID]; found { + backend.Logger.Info("Found duplicate plugin %s when registering API groups.", p.Primary.JSONData.ID) + continue + } + + uniquePlugins[p.Primary.JSONData.ID] = true + pluginJSONs = append(pluginJSONs, p.Primary.JSONData) + } } return pluginJSONs, nil } diff --git a/pkg/registry/apis/datasource/sub_health.go b/pkg/registry/apis/datasource/sub_health.go index 01a8ea82d9e..e80e355eb84 100644 --- a/pkg/registry/apis/datasource/sub_health.go +++ b/pkg/registry/apis/datasource/sub_health.go @@ -10,6 +10,8 @@ import ( "k8s.io/apiserver/pkg/registry/rest" datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type subHealthREST struct { @@ -44,7 +46,19 @@ func (r *subHealthREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, false, "" } +// FIXME: this endpoint has not been tested yet, so it is not enabled by default. +var healthEnabled = false + func (r *subHealthREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + if !healthEnabled { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotImplemented, + }, + } + } + pluginCtx, err := r.builder.getPluginContext(ctx, name) if err != nil { return nil, err diff --git a/pkg/registry/apis/datasource/sub_query.go b/pkg/registry/apis/datasource/sub_query.go index ce0c8cfd78b..a1bdddb6a30 100644 --- a/pkg/registry/apis/datasource/sub_query.go +++ b/pkg/registry/apis/datasource/sub_query.go @@ -13,7 +13,6 @@ import ( data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/errutil" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" - query_headers "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/web" @@ -91,7 +90,7 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob rsp, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{ Queries: queries, PluginContext: pluginCtx, - Headers: query_headers.ExtractKnownHeaders(req.Header), + Headers: map[string]string{}, }) // all errors get converted into k8 errors when sent in responder.Error and lose important context like downstream info diff --git a/pkg/registry/apis/datasource/sub_query_test.go b/pkg/registry/apis/datasource/sub_query_test.go index a3871ae954f..eeb1d9c87b4 100644 --- a/pkg/registry/apis/datasource/sub_query_test.go +++ b/pkg/registry/apis/datasource/sub_query_test.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "net/http" - "net/http/httptest" "testing" "github.com/stretchr/testify/require" @@ -16,53 +14,8 @@ import ( "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/ngalert/models" ) -func TestSubQueryConnect(t *testing.T) { - sqr := subQueryREST{ - builder: &DataSourceAPIBuilder{ - client: mockClient{ - lastCalledWithHeaders: &map[string]string{}, - }, - datasources: mockDatasources{}, - contextProvider: mockContextProvider{}, - }, - } - - mr := mockResponder{} - handler, err := sqr.Connect(context.Background(), "dsname", nil, mr) - require.NoError(t, err) - - rr := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/some-path", nil) - req.Header.Set(models.FromAlertHeaderName, "true") - req.Header.Set(models.CacheSkipHeaderName, "true") - req.Header.Set("X-Rule-Name", "name-1") - req.Header.Set("X-Rule-Uid", "abc") - req.Header.Set("X-Rule-Folder", "folder-1") - req.Header.Set("X-Rule-Source", "grafana-ruler") - req.Header.Set("X-Rule-Type", "type-1") - req.Header.Set("X-Rule-Version", "version-1") - req.Header.Set("X-Grafana-Org-Id", "1") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("some-unexpected-header", "some-value") - handler.ServeHTTP(rr, req) - - // test that headers are forwarded and cased appropriately - require.Equal(t, map[string]string{ - models.FromAlertHeaderName: "true", - models.CacheSkipHeaderName: "true", - "X-Rule-Name": "name-1", - "X-Rule-Uid": "abc", - "X-Rule-Folder": "folder-1", - "X-Rule-Source": "grafana-ruler", - "X-Rule-Type": "type-1", - "X-Rule-Version": "version-1", - "X-Grafana-Org-Id": "1", - }, *sqr.builder.client.(mockClient).lastCalledWithHeaders) -} - func TestSubQueryConnectWhenDatasourceNotFound(t *testing.T) { sqr := subQueryREST{ builder: &DataSourceAPIBuilder{ diff --git a/pkg/registry/apis/datasource/sub_resource.go b/pkg/registry/apis/datasource/sub_resource.go index f6d3fc04b96..02caab6995a 100644 --- a/pkg/registry/apis/datasource/sub_resource.go +++ b/pkg/registry/apis/datasource/sub_resource.go @@ -8,6 +8,7 @@ import ( "net/url" "strings" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" @@ -46,7 +47,21 @@ func (r *subResourceREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, true, "" } +// FIXME: this endpoint has not been tested yet, so it is not enabled by default. +// It is especially important to make sure the `ClearAuthHeadersMiddleware` is active, +// when using this endpoint. +var resourceEnabled = false + func (r *subResourceREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + if !resourceEnabled { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotImplemented, + }, + } + } + pluginCtx, err := r.builder.getPluginContext(ctx, name) if err != nil { return nil, err diff --git a/pkg/registry/apis/folders/register_test.go b/pkg/registry/apis/folders/register_test.go index fe472db4b9f..066f8793776 100644 --- a/pkg/registry/apis/folders/register_test.go +++ b/pkg/registry/apis/folders/register_test.go @@ -183,17 +183,85 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { func TestFolderAPIBuilder_Validate_Delete(t *testing.T) { tests := []struct { name string - statsResponse *resourcepb.ResourceStatsResponse_Stats + statsResponse []*resourcepb.ResourceStatsResponse_Stats wantErr bool }{ { - name: "should allow deletion when folder is empty", - statsResponse: &resourcepb.ResourceStatsResponse_Stats{Count: 0}, + name: "should allow deletion when folder is empty", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 0, Resource: "dashboards"}, + }, }, { - name: "should return folder not empty when the folder is not empty", - statsResponse: &resourcepb.ResourceStatsResponse_Stats{Count: 2}, - wantErr: true, + name: "should return folder not empty when folder contains dashboards", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 2, Resource: "dashboards", Group: "dashboard.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder contains alertrules", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 3, Resource: "alertrules", Group: "alerting.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder contains library_elements", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 1, Resource: "library_elements", Group: "library.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder contains folders", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 2, Resource: "folders", Group: "folders.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder has mixed resources with validated types", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 10, Resource: "folders", Group: "folders.grafana.app"}, + {Count: 2, Resource: "dashboards", Group: "dashboard.grafana.app"}, + {Count: 5, Resource: "playlists", Group: "playlist.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder has multiple validated resource types", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 1, Resource: "dashboards", Group: "dashboard.grafana.app"}, + {Count: 2, Resource: "alertrules", Group: "alerting.grafana.app"}, + {Count: 1, Resource: "library_elements", Group: "library.grafana.app"}, + {Count: 1, Resource: "folders", Group: "folders.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should allow deletion when all validated resource types are empty", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 0, Resource: "dashboards", Group: "dashboard.grafana.app"}, + {Count: 0, Resource: "alertrules", Group: "alerting.grafana.app"}, + {Count: 0, Resource: "library_elements", Group: "library.grafana.app"}, + {Count: 0, Resource: "folders", Group: "folders.grafana.app"}, + {Count: 10, Resource: "playlists", Group: "playlist.grafana.app"}, + }, + wantErr: false, + }, + { + name: "should allow deletion when folder only contains non-validated resource types", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 5, Resource: "playlists", Group: "playlist.grafana.app"}, + {Count: 3, Resource: "other", Group: "other.grafana.app"}, + }, + wantErr: false, + }, + { + name: "should allow deletion when stats array is empty", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{}, + wantErr: false, }, } @@ -212,7 +280,7 @@ func TestFolderAPIBuilder_Validate_Delete(t *testing.T) { us := grafanarest.NewMockStorage(t) sm := resource.NewMockResourceClient(t) sm.On("GetStats", mock.Anything, &resourcepb.ResourceStatsRequest{Namespace: obj.Namespace, Folder: obj.Name}).Return( - &resourcepb.ResourceStatsResponse{Stats: []*resourcepb.ResourceStatsResponse_Stats{tt.statsResponse}}, + &resourcepb.ResourceStatsResponse{Stats: tt.statsResponse}, nil, ).Once() diff --git a/pkg/registry/apis/folders/validate.go b/pkg/registry/apis/folders/validate.go index 53c32a356fe..4f8ccd2250d 100644 --- a/pkg/registry/apis/folders/validate.go +++ b/pkg/registry/apis/folders/validate.go @@ -120,6 +120,14 @@ func validateOnUpdate(ctx context.Context, return err } + // Check that the folder being moved is not an ancestor of the target parent. + // This prevents circular references (e.g., moving A under B when B is already under A). + for _, ancestor := range info.Items { + if ancestor.Name == obj.Name { + return fmt.Errorf("cannot move folder under its own descendant, this would create a circular reference") + } + } + // if by moving a folder we exceed the max depth, return an error if len(info.Items) > maxDepth+1 { return folder.ErrMaximumDepthReached.Errorf("maximum folder depth reached") @@ -144,9 +152,11 @@ func validateOnDelete(ctx context.Context, return fmt.Errorf("could not verify if folder is empty: %v", resp.Error) } + allowedResourceTypes := []string{"alertrules", "dashboards", "library_elements", "folders"} + for _, v := range resp.Stats { - if v.Count > 0 { - return folder.ErrFolderNotEmpty.Errorf("folder is not empty, contains %d resources", v.Count) + if slices.Contains(allowedResourceTypes, v.Resource) && v.Count > 0 { + return folder.ErrFolderNotEmpty.Errorf("folder is not empty, contains %d %s", v.Count, v.Resource) } } return nil diff --git a/pkg/registry/apis/folders/validate_test.go b/pkg/registry/apis/folders/validate_test.go index 2bed196ddce..1f67c0da9d8 100644 --- a/pkg/registry/apis/folders/validate_test.go +++ b/pkg/registry/apis/folders/validate_test.go @@ -264,6 +264,71 @@ func TestValidateUpdate(t *testing.T) { maxDepth: folder.MaxNestedFolderDepth, expectedErr: "[folder.maximum-depth-reached]", }, + { + name: "error when moving folder under its own descendant (direct child)", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "parent", + Annotations: map[string]string{ + utils.AnnoKeyFolder: "child", + }, + }, + Spec: folders.FolderSpec{ + Title: "parent folder", + }, + }, + old: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "parent", + }, + Spec: folders.FolderSpec{ + Title: "parent folder", + }, + }, + // When querying parents of "child", we get the chain: child -> parent -> root + // This means "parent" is an ancestor of "child", so we can't move "parent" under "child" + parents: &folders.FolderInfoList{ + Items: []folders.FolderInfo{ + {Name: "child", Parent: "parent"}, + {Name: "parent", Parent: folder.GeneralFolderUID}, + {Name: folder.GeneralFolderUID}, + }, + }, + expectedErr: "cannot move folder under its own descendant", + }, + { + name: "error when moving folder under its grandchild", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grandparent", + Annotations: map[string]string{ + utils.AnnoKeyFolder: "grandchild", + }, + }, + Spec: folders.FolderSpec{ + Title: "grandparent folder", + }, + }, + old: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grandparent", + }, + Spec: folders.FolderSpec{ + Title: "grandparent folder", + }, + }, + // When querying parents of "grandchild", we get: grandchild -> child -> grandparent -> root + // This means "grandparent" is in the ancestry, so we can't move it under "grandchild" + parents: &folders.FolderInfoList{ + Items: []folders.FolderInfo{ + {Name: "grandchild", Parent: "child"}, + {Name: "child", Parent: "grandparent"}, + {Name: "grandparent", Parent: folder.GeneralFolderUID}, + {Name: folder.GeneralFolderUID}, + }, + }, + expectedErr: "cannot move folder under its own descendant", + }, } for _, tt := range tests { @@ -320,7 +385,7 @@ func TestValidateDelete(t *testing.T) { }, }, }, { - name: "stats error", + name: "stats error - nil stats", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", @@ -331,7 +396,7 @@ func TestValidateDelete(t *testing.T) { }, expectedErr: "could not verify if folder is empty", }, { - name: "stats error", + name: "stats error - search error", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", @@ -342,7 +407,7 @@ func TestValidateDelete(t *testing.T) { }, expectedErr: "error running stats", }, { - name: "stats error", + name: "stats error - error result", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", @@ -357,7 +422,64 @@ func TestValidateDelete(t *testing.T) { }, expectedErr: "could not verify if folder is empty", }, { - name: "folder not empty", + name: "folder not empty - contains dashboards", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "dashboard.grafana.app", + Resource: "dashboards", + Count: 10, // not empty + }, + }, + }, + }, + expectedErr: "[folder.not-empty]", + }, { + name: "folder not empty - contains alertrules", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "alerting.grafana.app", + Resource: "alertrules", + Count: 5, // not empty + }, + }, + }, + }, + expectedErr: "[folder.not-empty]", + }, { + name: "folder not empty - contains library_elements", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "library.grafana.app", + Resource: "library_elements", + Count: 3, // not empty + }, + }, + }, + }, + expectedErr: "[folder.not-empty]", + }, { + name: "folder not empty - contains folders", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", @@ -369,7 +491,54 @@ func TestValidateDelete(t *testing.T) { { Group: "folders.grafana.app", Resource: "folders", - Count: 10, // not empty + Count: 2, // not empty + }, + }, + }, + }, + expectedErr: "[folder.not-empty]", + }, { + name: "folder can be deleted when it only contains non-validated resource types", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "playlist.grafana.app", + Resource: "playlists", + Count: 10, // has content but not a validated resource type + }, + { + Group: "other.grafana.app", + Resource: "other", + Count: 5, // has content but not a validated resource type + }, + }, + }, + }, + }, { + name: "folder not empty - mixed resources with validated types", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "folders.grafana.app", + Resource: "folders", + Count: 10, // now validated + }, + { + Group: "dashboard.grafana.app", + Resource: "dashboards", + Count: 2, // validated and has content }, }, }, diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 439f0de2da5..89f3f5ff2fa 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -24,7 +24,6 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth // Identity specific resources legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient) - resourceAuthorizer[iamv0.TeamResourceInfo.GetName()] = legacyAuthorizer resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = legacyAuthorizer resourceAuthorizer["display"] = legacyAuthorizer @@ -36,6 +35,8 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth resourceAuthorizer[iamv0.RoleBindingInfo.GetName()] = authorizer resourceAuthorizer[iamv0.ServiceAccountResourceInfo.GetName()] = authorizer resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer + resourceAuthorizer[iamv0.ExternalGroupMappingResourceInfo.GetName()] = authorizer + resourceAuthorizer[iamv0.TeamResourceInfo.GetName()] = authorizer return &iamAuthorizer{resourceAuthorizer: resourceAuthorizer} } diff --git a/pkg/registry/apis/iam/hooks.go b/pkg/registry/apis/iam/hooks.go deleted file mode 100644 index aac644dcfc3..00000000000 --- a/pkg/registry/apis/iam/hooks.go +++ /dev/null @@ -1,296 +0,0 @@ -package iam - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - "google.golang.org/protobuf/types/known/structpb" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - - iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" - "github.com/grafana/grafana/pkg/services/accesscontrol" - v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana" - "github.com/grafana/grafana/pkg/services/authz/zanzana/common" -) - -var ( - errEmptyName = errors.New("name cannot be empty") - errInvalidBasicRole = errors.New("invalid basic role") - errUnknownKind = errors.New("unknown permission kind") - - defaultWriteTimeout = 15 * time.Second -) - -func toZanzanaSubject(kind iamv0.ResourcePermissionSpecPermissionKind, name string) (string, error) { - if name == "" { - return "", errEmptyName - } - switch kind { - case iamv0.ResourcePermissionSpecPermissionKindUser: - return zanzana.NewTupleEntry(zanzana.TypeUser, name, ""), nil - case iamv0.ResourcePermissionSpecPermissionKindServiceAccount: - return zanzana.NewTupleEntry(zanzana.TypeServiceAccount, name, ""), nil - case iamv0.ResourcePermissionSpecPermissionKindTeam: - return zanzana.NewTupleEntry(zanzana.TypeTeam, name, ""), nil - case iamv0.ResourcePermissionSpecPermissionKindBasicRole: - basicRole := zanzana.TranslateBasicRole(name) - if basicRole == "" { - return "", fmt.Errorf("%w: %s", errInvalidBasicRole, name) - } - - // e.g role:basic_viewer#assignee - return zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, zanzana.RelationAssignee), nil - } - - // should not happen since we are after create - // validation webhook should have caught invalid kinds - return "", errUnknownKind -} - -func toZanzanaType(apiGroup string) string { - if apiGroup == "folder.grafana.app" { - return zanzana.TypeFolder - } - return zanzana.TypeResource -} - -func NewResourceTuple(object string, resource iamv0.ResourcePermissionspecResource, perm iamv0.ResourcePermissionspecPermission) (*v1.TupleKey, error) { - // Typ is "folder" or "resource" - typ := toZanzanaType(resource.ApiGroup) - - // subject - subject, err := toZanzanaSubject(perm.Kind, perm.Name) - if err != nil { - return nil, err - } - - key := &v1.TupleKey{ - // e.g. "user:{uid}", "serviceaccount:{uid}", "team:{uid}", "basicrole:{viewer|editor|admin}" - User: subject, - // "view", "edit", "admin" - Relation: strings.ToLower(perm.Verb), - // e.g. "folder:{name}" or "resource:{apiGroup}/{resource}/{name}" - Object: object, - } - - // For resources we add a condition to filter by apiGroup/resource - // e.g "group_filter": {"group_resource": "dashboards.grafana.app/dashboards"} - if typ == zanzana.TypeResource { - key.Condition = &v1.RelationshipCondition{ - Name: "group_filter", - Context: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "group_resource": structpb.NewStringValue( - resource.ApiGroup + "/" + resource.Resource, - ), - }, - }, - } - } - - return key, nil -} - -// AfterResourcePermissionCreate is a post-create hook that writes the resource permission to Zanzana (openFGA) -func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj runtime.Object, _ *metav1.CreateOptions) { - if b.zClient == nil { - return - } - - rp, ok := obj.(*iamv0.ResourcePermission) - if !ok { - return - } - - // Grab a ticket to write to Zanzana - // This limits the amount of concurrent writes to Zanzana - wait := time.Now() - b.zTickets <- true - hooksWaitHistogram.Observe(time.Since(wait).Seconds()) // Record wait time - - go func(rp *iamv0.ResourcePermission) { - defer func() { - // Release the ticket after write is done - <-b.zTickets - }() - - resource := rp.Spec.Resource - permissions := rp.Spec.Permissions - - object := zanzana.NewObjectEntry(toZanzanaType(resource.ApiGroup), resource.ApiGroup, resource.Resource, "", resource.Name) - - tuples := make([]*v1.TupleKey, 0, len(permissions)) - for _, p := range permissions { - tuple, err := NewResourceTuple(object, resource, p) - if err != nil { - b.logger.Error("failed to create resource permission tuple", - "namespace", rp.Namespace, - "object", object, - "err", err, - ) - - continue - } - tuples = append(tuples, tuple) - } - - // Avoid writing if there are no valid tuples - if len(tuples) == 0 { - b.logger.Warn("no valid tuples to write", "namespace", rp.Namespace, "resource", object) - return - } - - b.logger.Debug("writing resource permission to zanzana", - "namespace", rp.Namespace, - "object", object, - "tuplesCnt", len(tuples), - ) - - ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) - defer cancel() - - err := b.zClient.Write(ctx, &v1.WriteRequest{ - Namespace: rp.Namespace, - Writes: &v1.WriteRequestWrites{ - TupleKeys: tuples, - }, - }) - if err != nil { - b.logger.Error("failed to write resource permission to zanzana", - "err", err, - "namespace", rp.Namespace, - "object", object, - "tuplesCnt", len(tuples), - ) - } - }(rp.DeepCopy()) // Pass a copy of the object -} - -// convertRolePermissionsToTuples converts role permissions (action/scope) to v1 TupleKey format -// using the shared zanzana.ConvertRolePermissionsToTuples utility and common.ToAuthzExtTupleKeys -func convertRolePermissionsToTuples(roleUID string, permissions []iamv0.CoreRolespecPermission) ([]*v1.TupleKey, error) { - // Convert IAM permissions to zanzana.RolePermission format - rolePerms := make([]zanzana.RolePermission, 0, len(permissions)) - for _, perm := range permissions { - // Split the scope to get kind, attribute, identifier - kind, _, identifier := accesscontrol.SplitScope(perm.Scope) - rolePerms = append(rolePerms, zanzana.RolePermission{ - Action: perm.Action, - Kind: kind, - Identifier: identifier, - }) - } - - // Translate to Zanzana tuples - openfgaTuples, err := zanzana.ConvertRolePermissionsToTuples(roleUID, rolePerms) - if err != nil { - return nil, err - } - - // Convert directly to v1 tuples using common utility - v1Tuples := common.ToAuthzExtTupleKeys(openfgaTuples) - - return v1Tuples, nil -} - -// AfterRoleCreate is a post-create hook that writes the role permissions to Zanzana (openFGA) -// It handles both Role and CoreRole types -func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object, _ *metav1.CreateOptions) { - if b.zClient == nil { - return - } - - // Extract permissions based on the object type - var roleUID, namespace string - var permissions []iamv0.CoreRolespecPermission - var roleType string - - // Try CoreRole first - if coreRole, ok := obj.(*iamv0.CoreRole); ok { - roleUID = coreRole.Name - namespace = coreRole.Namespace - // Deep copy permissions to avoid race conditions - permissions = make([]iamv0.CoreRolespecPermission, len(coreRole.Spec.Permissions)) - copy(permissions, coreRole.Spec.Permissions) - roleType = "core role" - } else if role, ok := obj.(*iamv0.Role); ok { - // Try Role - roleUID = role.Name - namespace = role.Namespace - - // Convert and copy permissions to avoid race conditions - permissions = make([]iamv0.CoreRolespecPermission, len(role.Spec.Permissions)) - for i, p := range role.Spec.Permissions { - permissions[i] = iamv0.CoreRolespecPermission(p) - } - roleType = "role" - } else { - // Not a supported role type - return - } - - wait := time.Now() - b.zTickets <- true - hooksWaitHistogram.Observe(time.Since(wait).Seconds()) - - go func() { - defer func() { - <-b.zTickets - }() - - tuples, err := convertRolePermissionsToTuples(roleUID, permissions) - if err != nil { - b.logger.Error("failed to convert role permissions to tuples", - "namespace", namespace, - "roleUID", roleUID, - "roleType", roleType, - "err", err, - "permissionsCnt", len(permissions), - ) - return - } - - // Avoid writing if there are no valid tuples - if len(tuples) == 0 { - b.logger.Debug("no valid tuples to write for role", - "namespace", namespace, - "roleUID", roleUID, - "roleType", roleType, - "permissionsCnt", len(permissions), - ) - return - } - - b.logger.Debug("writing role permissions to zanzana", - "namespace", namespace, - "roleUID", roleUID, - "roleType", roleType, - "tuplesCnt", len(tuples), - "permissionsCnt", len(permissions), - ) - - ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) - defer cancel() - - err = b.zClient.Write(ctx, &v1.WriteRequest{ - Namespace: namespace, - Writes: &v1.WriteRequestWrites{ - TupleKeys: tuples, - }, - }) - if err != nil { - b.logger.Error("failed to write role permissions to zanzana", - "err", err, - "namespace", namespace, - "roleUID", roleUID, - "roleType", roleType, - "tuplesCnt", len(tuples), - ) - } - }() -} diff --git a/pkg/registry/apis/iam/hooks_test.go b/pkg/registry/apis/iam/hooks_test.go deleted file mode 100644 index 9c783171560..00000000000 --- a/pkg/registry/apis/iam/hooks_test.go +++ /dev/null @@ -1,435 +0,0 @@ -package iam - -import ( - "context" - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" - "github.com/grafana/grafana/pkg/infra/log" - v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana" - "github.com/stretchr/testify/require" -) - -type FakeZanzanaClient struct { - zanzana.Client - writeCallback func(context.Context, *v1.WriteRequest) error -} - -// Write implements zanzana.Client. -func (f *FakeZanzanaClient) Write(ctx context.Context, req *v1.WriteRequest) error { - return f.writeCallback(ctx, req) -} - -func requireTuplesMatch(t *testing.T, actual []*v1.TupleKey, expected []*v1.TupleKey, msgAndArgs ...interface{}) { - t.Helper() - for _, exp := range expected { - found := false - for _, act := range actual { - if act.User == exp.User && - act.Relation == exp.Relation && - act.Object == exp.Object { - found = true - break - } - } - if !found { - require.Fail(t, "Expected tuple not found", "Tuple: %+v\n%v", exp, msgAndArgs) - } - } -} - -func TestAfterResourcePermissionCreate(t *testing.T) { - t.Run("should create zanzana entries for folder resource permissions", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - folderPerm := iamv0.ResourcePermission{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "org-2", - }, - Spec: iamv0.ResourcePermissionSpec{ - Resource: iamv0.ResourcePermissionspecResource{ - ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1", - }, - Permissions: []iamv0.ResourcePermissionspecPermission{ - {Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"}, - {Kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "Edit"}, - }, - }, - } - - testFolderEntries := func(ctx context.Context, req *v1.WriteRequest) error { - require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 2) - require.Equal(t, "org-2", req.Namespace) - - expectedTuples := []*v1.TupleKey{ - {User: "user:u1", Relation: "view", Object: "folder:fold1"}, - {User: "role:basic_editor#assignee", Relation: "edit", Object: "folder:fold1"}, - } - - requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples) - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testFolderEntries} - b.AfterResourcePermissionCreate(&folderPerm, nil) - }) - - t.Run("should create zanzana entries for dashboard resource permissions", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - dashPerm := iamv0.ResourcePermission{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - }, - Spec: iamv0.ResourcePermissionSpec{ - Resource: iamv0.ResourcePermissionspecResource{ - ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", - }, - Permissions: []iamv0.ResourcePermissionspecPermission{ - {Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "View"}, - {Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "Edit"}, - }, - }, - } - - testDashEntries := func(ctx context.Context, req *v1.WriteRequest) error { - object := "resource:dashboard.grafana.app/dashboards/dash1" - - require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 2) - require.Equal(t, "default", req.Namespace) - - // Verify all tuples have the group_filter condition - for _, tuple := range req.Writes.TupleKeys { - require.NotNil(t, tuple.Condition, "Condition should not be nil for tuple %+v", tuple) - require.Equal(t, "group_filter", tuple.Condition.Name) - } - - expectedTuples := []*v1.TupleKey{ - {User: "service-account:sa1", Relation: "view", Object: object}, - {User: "team:team1", Relation: "edit", Object: object}, - } - - requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples) - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testDashEntries} - b.AfterResourcePermissionCreate(&dashPerm, nil) - }) -} - -func TestAfterCoreRoleCreate(t *testing.T) { - t.Run("should create zanzana entries for core role with folder permissions", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - coreRole := iamv0.CoreRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-role-uid", - Namespace: "org-1", - }, - Spec: iamv0.CoreRoleSpec{ - Title: "Test Role", - Description: "Test role for folders", - Permissions: []iamv0.CoreRolespecPermission{ - {Action: "folders:read", Scope: "folders:uid:folder1"}, - {Action: "folders:write", Scope: "folders:uid:folder1"}, - }, - }, - } - - testCoreRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error { - require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 2) - require.Equal(t, "org-1", req.Namespace) - - expectedTuples := []*v1.TupleKey{ - {User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder1"}, - {User: "role:test-role-uid#assignee", Relation: "update", Object: "folder:folder1"}, - } - - requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples) - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleEntries} - b.AfterRoleCreate(&coreRole, nil) - }) - - t.Run("should create zanzana entries for core role with dashboard permissions", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - coreRole := iamv0.CoreRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: "dashboard-role-uid", - Namespace: "default", - }, - Spec: iamv0.CoreRoleSpec{ - Title: "Dashboard Role", - Description: "Test role for dashboards", - Permissions: []iamv0.CoreRolespecPermission{ - {Action: "dashboards:read", Scope: "dashboards:uid:dash1"}, - {Action: "dashboards:write", Scope: "dashboards:uid:dash1"}, - }, - }, - } - - testDashboardRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error { - require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 2) - require.Equal(t, "default", req.Namespace) - - // Check subject is role with assignee relation - for _, tuple := range req.Writes.TupleKeys { - require.Equal(t, "role:dashboard-role-uid#assignee", tuple.User) - require.Contains(t, tuple.Object, "resource:") - require.Contains(t, tuple.Object, "dashboard") - } - - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleEntries} - b.AfterRoleCreate(&coreRole, nil) - }) - - t.Run("should handle wildcard scopes", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - coreRole := iamv0.CoreRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: "wildcard-role-uid", - Namespace: "org-2", - }, - Spec: iamv0.CoreRoleSpec{ - Title: "Wildcard Role", - Permissions: []iamv0.CoreRolespecPermission{ - {Action: "folders:read", Scope: "folders:*"}, - }, - }, - } - - testWildcardEntries := func(ctx context.Context, req *v1.WriteRequest) error { - require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 1) - - tuple := req.Writes.TupleKeys[0] - require.Equal(t, "role:wildcard-role-uid#assignee", tuple.User) - // Wildcard should create a group_resource tuple - require.Contains(t, tuple.Object, "group_resource:") - - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testWildcardEntries} - b.AfterRoleCreate(&coreRole, nil) - }) - - t.Run("should skip untranslatable permissions", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - coreRole := iamv0.CoreRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: "mixed-role-uid", - Namespace: "org-1", - }, - Spec: iamv0.CoreRoleSpec{ - Title: "Mixed Role", - Permissions: []iamv0.CoreRolespecPermission{ - {Action: "folders:read", Scope: "folders:uid:folder1"}, - {Action: "unknown:action", Scope: "unknown:scope"}, // This should be skipped - }, - }, - } - - testMixedEntries := func(ctx context.Context, req *v1.WriteRequest) error { - require.NotNil(t, req) - require.NotNil(t, req.Writes) - // Should only have 1 tuple (the untranslatable one should be skipped) - require.Len(t, req.Writes.TupleKeys, 1) - - tuple := req.Writes.TupleKeys[0] - require.Equal(t, "role:mixed-role-uid#assignee", tuple.User) - require.Equal(t, "folder:folder1", tuple.Object) - - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testMixedEntries} - b.AfterRoleCreate(&coreRole, nil) - }) -} - -func TestAfterRoleCreate(t *testing.T) { - t.Run("should create zanzana entries for role with folder permissions", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - role := iamv0.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: "custom-role-uid", - Namespace: "org-3", - }, - Spec: iamv0.RoleSpec{ - Title: "Custom Role", - Description: "Custom role for folders", - Permissions: []iamv0.RolespecPermission{ - {Action: "folders:read", Scope: "folders:uid:folder2"}, - {Action: "folders:delete", Scope: "folders:uid:folder2"}, - }, - }, - } - - testRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error { - require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 2) - require.Equal(t, "org-3", req.Namespace) - - expectedTuples := []*v1.TupleKey{ - {User: "role:custom-role-uid#assignee", Relation: "get", Object: "folder:folder2"}, - {User: "role:custom-role-uid#assignee", Relation: "delete", Object: "folder:folder2"}, - } - - requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples) - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testRoleEntries} - b.AfterRoleCreate(&role, nil) - }) - - t.Run("should create zanzana entries for role with dashboard permissions", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - role := iamv0.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: "dash-role-uid", - Namespace: "default", - }, - Spec: iamv0.RoleSpec{ - Title: "Dashboard Custom Role", - Description: "Custom role for dashboards", - Permissions: []iamv0.RolespecPermission{ - {Action: "dashboards:read", Scope: "dashboards:uid:mydash"}, - {Action: "dashboards:delete", Scope: "dashboards:uid:mydash"}, - }, - }, - } - - testDashRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error { - require.NotNil(t, req) - require.NotNil(t, req.Writes) - require.Len(t, req.Writes.TupleKeys, 2) - require.Equal(t, "default", req.Namespace) - - // Check subject is role with assignee relation - for _, tuple := range req.Writes.TupleKeys { - require.Equal(t, "role:dash-role-uid#assignee", tuple.User) - require.Contains(t, tuple.Object, "resource:") - } - - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testDashRoleEntries} - b.AfterRoleCreate(&role, nil) - }) - - t.Run("should merge folder resource tuples with same object and user", func(t *testing.T) { - b := &IdentityAccessManagementAPIBuilder{ - logger: log.NewNopLogger(), - zTickets: make(chan bool, 1), - } - t.Cleanup(func() { - <-b.zTickets - }) - - role := iamv0.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: "merge-role-uid", - Namespace: "org-1", - }, - Spec: iamv0.RoleSpec{ - Title: "Merge Test Role", - Permissions: []iamv0.RolespecPermission{ - // These should create folder resource tuples that get merged - {Action: "dashboards:read", Scope: "folders:uid:parent-folder"}, - {Action: "dashboards:write", Scope: "folders:uid:parent-folder"}, - }, - }, - } - - testMergedEntries := func(ctx context.Context, req *v1.WriteRequest) error { - require.NotNil(t, req) - require.NotNil(t, req.Writes) - // After merging, we should have tuples for the folder resource actions - require.Greater(t, len(req.Writes.TupleKeys), 0) - - for _, tuple := range req.Writes.TupleKeys { - require.Equal(t, "role:merge-role-uid#assignee", tuple.User) - } - - return nil - } - - b.zClient = &FakeZanzanaClient{writeCallback: testMergedEntries} - b.AfterRoleCreate(&role, nil) - }) -} diff --git a/pkg/registry/apis/iam/legacy/team.go b/pkg/registry/apis/iam/legacy/team.go index 51e68506fbd..874dea3dff9 100644 --- a/pkg/registry/apis/iam/legacy/team.go +++ b/pkg/registry/apis/iam/legacy/team.go @@ -2,6 +2,7 @@ package legacy import ( "context" + "database/sql" "errors" "fmt" "time" @@ -129,18 +130,18 @@ func (s *legacySQLStore) ListTeams(ctx context.Context, ns claims.NamespaceInfo, return nil, fmt.Errorf("expected non zero orgID") } - sql, err := s.sql(ctx) + sqlConn, err := s.sql(ctx) if err != nil { return nil, err } - req := newListTeams(sql, &query) + req := newListTeams(sqlConn, &query) q, err := sqltemplate.Execute(sqlQueryTeamsTemplate, req) if err != nil { return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeamsTemplate.Name(), err) } - rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + rows, err := sqlConn.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) defer func() { if rows != nil { _ = rows.Close() @@ -155,11 +156,21 @@ func (s *legacySQLStore) ListTeams(ctx context.Context, ns claims.NamespaceInfo, var lastID int64 for rows.Next() { t := team.Team{} - err = rows.Scan(&t.ID, &t.UID, &t.Name, &t.Email, &t.ExternalUID, &t.IsProvisioned, &t.Created, &t.Updated) + var externalUID sql.NullString + var isProvisioned sql.NullBool + err = rows.Scan(&t.ID, &t.UID, &t.Name, &t.Email, &externalUID, &isProvisioned, &t.Created, &t.Updated) if err != nil { return res, err } + if externalUID.Valid { + t.ExternalUID = externalUID.String + } + + if isProvisioned.Valid { + t.IsProvisioned = isProvisioned.Bool + } + lastID = t.ID res.Teams = append(res.Teams, t) if len(res.Teams) > int(query.Pagination.Limit)-1 { @@ -170,7 +181,7 @@ func (s *legacySQLStore) ListTeams(ctx context.Context, ns claims.NamespaceInfo, } if query.UID == "" { - res.RV, err = sql.GetResourceVersion(ctx, "team", "updated") + res.RV, err = sqlConn.GetResourceVersion(ctx, "team", "updated") } return res, err diff --git a/pkg/registry/apis/iam/metrics.go b/pkg/registry/apis/iam/metrics.go index 6c25a8746f7..aa918732843 100644 --- a/pkg/registry/apis/iam/metrics.go +++ b/pkg/registry/apis/iam/metrics.go @@ -14,19 +14,53 @@ const ( var ( registerOnce sync.Once - hooksWaitHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ + hooksWaitHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: metricsNamespace, Subsystem: metricsSubSystem, Name: "hooks_wait_duration_seconds", Help: "Time spent in the hooks waiting for a ticket to start processing", Buckets: prometheus.ExponentialBuckets(0.001, 2, 5), // 1ms to ~16s - }) + }, []string{"resource_type", "operation"}) + + // hooksDurationHistogram tracks the total duration of hook operations + hooksDurationHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Name: "hooks_operation_duration_seconds", + Help: "Time spent executing hook operations (create, update, delete)", + Buckets: prometheus.ExponentialBuckets(0.001, 2, 10), // 1ms to ~1s + }, []string{"resource_type", "operation", "status"}) + + // hooksOperationCounter tracks the number of hook operations + hooksOperationCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Name: "hooks_operations_total", + Help: "Total number of hook operations by resource type, operation, and status", + }, []string{"resource_type", "operation", "status"}) + + // hooksTuplesCounter tracks the number of tuples written/deleted + hooksTuplesCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Name: "hooks_tuples_total", + Help: "Total number of tuples written or deleted by resource type and operation type", + }, []string{"resource_type", "operation", "action"}) ) func registerMetrics(reg prometheus.Registerer) { registerOnce.Do(func() { - if err := reg.Register(hooksWaitHistogram); err != nil { - log.New("iam.apis").Warn("failed to register iam apiserver metrics", "error", err) + metrics := []prometheus.Collector{ + hooksWaitHistogram, + hooksDurationHistogram, + hooksOperationCounter, + hooksTuplesCounter, + } + + for _, metric := range metrics { + if err := reg.Register(metric); err != nil { + log.New("iam.apis").Warn("failed to register iam apiserver metrics", "error", err) + } } }) } diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index 632a3f15a9a..c960b7a8655 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -35,14 +35,19 @@ type RoleStorageBackend interface{ resource.StorageBackend } // Used by wire to identify the storage backend for role bindings. type RoleBindingStorageBackend interface{ resource.StorageBackend } +// ExternalGroupMappingStorageBackend uses the resource.StorageBackend interface to provide storage for external group mappings. +// Used by wire to identify the storage backend for external group mappings. +type ExternalGroupMappingStorageBackend interface{ resource.StorageBackend } + // This is used just so wire has something unique to return type IdentityAccessManagementAPIBuilder struct { // Stores - store legacy.LegacyIdentityStore - coreRolesStorage CoreRoleStorageBackend - rolesStorage RoleStorageBackend - resourcePermissionsStorage resource.StorageBackend - roleBindingsStorage RoleBindingStorageBackend + store legacy.LegacyIdentityStore + coreRolesStorage CoreRoleStorageBackend + rolesStorage RoleStorageBackend + resourcePermissionsStorage resource.StorageBackend + roleBindingsStorage RoleBindingStorageBackend + externalGroupMappingStorage ExternalGroupMappingStorageBackend // Access Control authorizer authorizer.Authorizer diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index c852515f75c..e6b58f3c536 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -30,6 +30,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" + "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" "github.com/grafana/grafana/pkg/registry/apis/iam/sso" @@ -63,6 +64,7 @@ func RegisterAPIService( coreRolesStorage CoreRoleStorageBackend, rolesStorage RoleStorageBackend, roleBindingsStorage RoleBindingStorageBackend, + externalGroupMappingStorageBackend ExternalGroupMappingStorageBackend, dual dualwrite.Service, unified resource.ResourceClient, userService legacyuser.Service, @@ -74,46 +76,54 @@ func RegisterAPIService( registerMetrics(reg) builder := &IdentityAccessManagementAPIBuilder{ - store: store, - coreRolesStorage: coreRolesStorage, - rolesStorage: rolesStorage, - resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), - roleBindingsStorage: roleBindingsStorage, - sso: ssoService, - authorizer: authorizer, - legacyAccessClient: legacyAccessClient, - accessClient: accessClient, - zClient: zClient, - zTickets: make(chan bool, MaxConcurrentZanzanaWrites), - display: user.NewLegacyDisplayREST(store), - reg: reg, - logger: log.New("iam.apis"), - features: features, - enableDualWriter: true, - dual: dual, - unified: unified, - userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), unified, user.NewUserLegacySearchClient(userService), features), + store: store, + coreRolesStorage: coreRolesStorage, + rolesStorage: rolesStorage, + resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), + roleBindingsStorage: roleBindingsStorage, + externalGroupMappingStorage: externalGroupMappingStorageBackend, + sso: ssoService, + authorizer: authorizer, + legacyAccessClient: legacyAccessClient, + accessClient: accessClient, + zClient: zClient, + zTickets: make(chan bool, MaxConcurrentZanzanaWrites), + display: user.NewLegacyDisplayREST(store), + reg: reg, + logger: log.New("iam.apis"), + features: features, + enableDualWriter: true, + dual: dual, + unified: unified, + userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), unified, user.NewUserLegacySearchClient(userService), features), } apiregistration.RegisterAPI(builder) return builder, nil } -// TODO zClient, zTickets, reg func NewAPIService( accessClient types.AccessClient, dbProvider legacysql.LegacyDatabaseProvider, features featuremgmt.FeatureToggles, + zClient zanzana.Client, + reg prometheus.Registerer, ) *IdentityAccessManagementAPIBuilder { store := legacy.NewLegacySQLStores(dbProvider) resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider) resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient) + noopStorage := noopstorage.ProvideStorageBackend() + registerMetrics(reg) return &IdentityAccessManagementAPIBuilder{ - store: store, - display: user.NewLegacyDisplayREST(store), - resourcePermissionsStorage: resourcePermissionsStorage, - logger: log.New("iam.apis"), - features: features, + store: store, + display: user.NewLegacyDisplayREST(store), + resourcePermissionsStorage: resourcePermissionsStorage, + externalGroupMappingStorage: noopStorage, + logger: log.New("iam.apis"), + features: features, + zClient: zClient, + zTickets: make(chan bool, MaxConcurrentZanzanaWrites), + reg: reg, authorizer: authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { // For now only authorize resourcepermissions resource @@ -219,6 +229,14 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge return err } + // Only teamBindingStore exposes the AfterCreate, AfterDelete, and BeginUpdate hooks + if enableZanzanaSync { + b.logger.Info("Enabling hooks for TeamBinding to sync to Zanzana") + teamBindingStore.AfterCreate = b.AfterTeamBindingCreate + teamBindingStore.AfterDelete = b.AfterTeamBindingDelete + teamBindingStore.BeginUpdate = b.BeginTeamBindingUpdate + } + storage[teamBindingResource.StoragePath()] = teamBindingDW } @@ -233,6 +251,13 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge return err } + if enableZanzanaSync { + b.logger.Info("Enabling hooks for User to sync basic role assignments to Zanzana") + store.AfterCreate = b.AfterUserCreate + store.BeginUpdate = b.BeginUserUpdate + store.AfterDelete = b.AfterUserDelete + } + dw, err := opts.DualWriteBuilder(userResource.GroupResource(), legacyStore, store) if err != nil { return err @@ -269,6 +294,27 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso) } + externalGroupMappingResource := iamv0.ExternalGroupMappingResourceInfo + externalGroupMappingLegacyStore, err := NewLocalStore(externalGroupMappingResource, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.externalGroupMappingStorage) + if err != nil { + return err + } + storage[externalGroupMappingResource.StoragePath()] = externalGroupMappingLegacyStore + + if b.enableDualWriter { + externalGroupMappingStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, externalGroupMappingResource, opts.OptsGetter) + if err != nil { + return err + } + + externalGroupMappingDW, err := opts.DualWriteBuilder(externalGroupMappingResource.GroupResource(), externalGroupMappingLegacyStore, externalGroupMappingStore) + if err != nil { + return err + } + + storage[externalGroupMappingResource.StoragePath()] = externalGroupMappingDW + } + //nolint:staticcheck // not yet migrated to OpenFeature if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) { // v0alpha1 @@ -277,8 +323,10 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge return err } if enableZanzanaSync { - b.logger.Info("Enabling AfterCreate hook for CoreRole to sync to Zanzana") + b.logger.Info("Enabling hooks for CoreRole to sync to Zanzana") coreRoleStore.AfterCreate = b.AfterRoleCreate + coreRoleStore.AfterDelete = b.AfterRoleDelete + coreRoleStore.BeginUpdate = b.BeginRoleUpdate } storage[iamv0.CoreRoleInfo.StoragePath()] = coreRoleStore @@ -287,8 +335,10 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge return err } if enableZanzanaSync { - b.logger.Info("Enabling AfterCreate hook for Role to sync to Zanzana") + b.logger.Info("Enabling hooks for Role to sync to Zanzana") roleStore.AfterCreate = b.AfterRoleCreate + roleStore.AfterDelete = b.AfterRoleDelete + roleStore.BeginUpdate = b.BeginRoleUpdate } storage[iamv0.RoleInfo.StoragePath()] = roleStore @@ -300,21 +350,59 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } //nolint:staticcheck // not yet migrated to OpenFeature if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) { - resourcePermissionStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage) - if err != nil { + if err := b.UpdateResourcePermissionsAPIGroup(apiGroupInfo, opts, storage, b.enableDualWriter, enableZanzanaSync); err != nil { return err } - if enableZanzanaSync { - b.logger.Info("Enabling AfterCreate hook for ResourcePermission to sync to Zanzana") - resourcePermissionStore.AfterCreate = b.AfterResourcePermissionCreate - } - storage[iamv0.ResourcePermissionInfo.StoragePath()] = resourcePermissionStore } apiGroupInfo.VersionedResourcesStorageMap[legacyiamv0.VERSION] = storage return nil } +func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup( + apiGroupInfo *genericapiserver.APIGroupInfo, + opts builder.APIGroupOptions, + storage map[string]rest.Storage, + enableDualWriter bool, + enableZanzanaSync bool, +) error { + var store rest.Storage + // Create the legacy store first + legacyStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage) + if err != nil { + return err + } + + // Register the hooks for Zanzana sync + // FIXME: The hooks are registered on the legacy store + // Once we fully migrate to unified storage, we can move these hooks to the unified store + if enableZanzanaSync { + b.logger.Info("Enabling AfterCreate, BeginUpdate, and AfterDelete hooks for ResourcePermission to sync to Zanzana") + legacyStore.AfterCreate = b.AfterResourcePermissionCreate + legacyStore.BeginUpdate = b.BeginResourcePermissionUpdate + legacyStore.AfterDelete = b.AfterResourcePermissionDelete + } + + // Set the default store to the legacy store + store = legacyStore + + if enableDualWriter { + // Create the dual write store (UniStore + LegacyStore) + uniStore, err := grafanaregistry.NewRegistryStore(apiGroupInfo.Scheme, iamv0.ResourcePermissionInfo, opts.OptsGetter) + if err != nil { + return err + } + + store, err = opts.DualWriteBuilder(iamv0.ResourcePermissionInfo.GroupResource(), legacyStore, uniStore) + if err != nil { + return err + } + } + + storage[iamv0.ResourcePermissionInfo.StoragePath()] = store + return nil +} + func (b *IdentityAccessManagementAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { return func(rc common.ReferenceCallback) map[string]common.OpenAPIDefinition { dst := legacyiamv0.GetOpenAPIDefinitions(rc) diff --git a/pkg/registry/apis/iam/resource_permission_hooks.go b/pkg/registry/apis/iam/resource_permission_hooks.go new file mode 100644 index 00000000000..e725726b2ec --- /dev/null +++ b/pkg/registry/apis/iam/resource_permission_hooks.go @@ -0,0 +1,346 @@ +package iam + +import ( + "context" + "errors" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic/registry" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +var ( + errEmptyName = errors.New("name cannot be empty") + + defaultWriteTimeout = 15 * time.Second +) + +// AfterResourcePermissionCreate is a post-create hook that writes the resource permission to Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj runtime.Object, _ *metav1.CreateOptions) { + if b.zClient == nil { + return + } + + rp, ok := obj.(*iamv0.ResourcePermission) + if !ok { + b.logger.Error("failed to convert object to resourcePermission type", "object", obj) + return + } + + resourceType := "resourcepermission" + operation := "create" + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) // Record wait time + + go func(rp *iamv0.ResourcePermission) { + start := time.Now() + status := "success" + + defer func() { + // Release the ticket after write is done + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc() + }() + + resource := rp.Spec.Resource + permissions := rp.Spec.Permissions + + operations := make([]*v1.MutateOperation, 0, len(permissions)) + for _, p := range permissions { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: resource.ApiGroup, + Resource: resource.Resource, + Name: resource.Name, + }, + Permission: &v1.Permission{ + Kind: string(p.Kind), + Name: p.Name, + Verb: p.Verb, + }, + }, + }, + }) + } + + if len(operations) == 0 { + return + } + + b.logger.Debug("writing resource permission to zanzana", + "namespace", rp.Namespace, + "resource", resource, + "operationsCount", len(operations), + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: rp.Namespace, + Operations: operations, + }) + if err != nil { + status = "failure" + b.logger.Error("failed to write resource permission to zanzana", + "err", err, + "namespace", rp.Namespace, + "resource", resource, + "operationsCount", len(operations), + ) + } else { + // Record successful tuple writes + hooksTuplesCounter.WithLabelValues(resourceType, operation, "write").Add(float64(len(operations))) + } + }(rp.DeepCopy()) // Pass a copy of the object +} + +// BeginResourcePermissionUpdate is a pre-update hook that prepares zanzana updates +// It converts old and new permissions to tuples and performs the zanzana write after K8s update succeeds +func (b *IdentityAccessManagementAPIBuilder) BeginResourcePermissionUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) { + if b.zClient == nil { + return nil, nil + } + + // Extract permissions from both old and new objects + oldRP, ok := oldObj.(*iamv0.ResourcePermission) + if !ok { + return nil, nil + } + + newRP, ok := obj.(*iamv0.ResourcePermission) + if !ok { + return nil, nil + } + + // Convert old permissions to delete operations + deleteOperations := make([]*v1.MutateOperation, 0, len(oldRP.Spec.Permissions)) + if len(oldRP.Spec.Permissions) > 0 { + oldResource := oldRP.Spec.Resource + for _, p := range oldRP.Spec.Permissions { + deleteOperations = append(deleteOperations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: oldResource.ApiGroup, + Resource: oldResource.Resource, + Name: oldResource.Name, + }, + Permission: &v1.Permission{ + Kind: string(p.Kind), + Name: p.Name, + Verb: p.Verb, + }, + }, + }, + }) + } + } + + // Convert new permissions to create operations + createOperations := make([]*v1.MutateOperation, 0, len(newRP.Spec.Permissions)) + if len(newRP.Spec.Permissions) > 0 { + newResource := newRP.Spec.Resource + for _, p := range newRP.Spec.Permissions { + createOperations = append(createOperations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: newResource.ApiGroup, + Resource: newResource.Resource, + Name: newResource.Name, + }, + Permission: &v1.Permission{ + Kind: string(p.Kind), + Name: p.Name, + Verb: p.Verb, + }, + }, + }, + }) + } + } + + // Return a finish function that performs the zanzana write only on success + return func(ctx context.Context, success bool) { + if !success { + // Update failed, don't write to zanzana + return + } + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues("resourcepermission", "update").Observe(time.Since(wait).Seconds()) + + go func() { + start := time.Now() + status := "success" + + defer func() { + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues("resourcepermission", "update", status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues("resourcepermission", "update", status).Inc() + }() + + b.logger.Debug("updating resource permission in zanzana", + "namespace", newRP.Namespace, + "oldPermissionsCnt", len(oldRP.Spec.Permissions), + "newPermissionsCnt", len(newRP.Spec.Permissions), + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + // Prepare write request + req := &v1.MutateRequest{ + Namespace: newRP.Namespace, + Operations: append(deleteOperations, createOperations...), + } + + if len(req.Operations) == 0 { + return + } + + if len(deleteOperations) > 0 { + b.logger.Debug("deleting existing resource permissions from zanzana", + "namespace", newRP.Namespace, + "operationsCount", len(deleteOperations), + ) + } + + if len(createOperations) > 0 { + b.logger.Debug("writing new resource permissions to zanzana", + "namespace", newRP.Namespace, + "operationsCount", len(createOperations), + ) + } + + // Only make the request if there are deletes or writes + if len(req.Operations) > 0 { + err := b.zClient.Mutate(ctx, req) + if err != nil { + status = "failure" + b.logger.Error("failed to update resource permission in zanzana", + "err", err, + "namespace", newRP.Namespace, + ) + } else { + // Record successful tuple operations + if len(deleteOperations) > 0 { + hooksTuplesCounter.WithLabelValues("resourcepermission", "update", "delete").Add(float64(len(deleteOperations))) + } + if len(createOperations) > 0 { + hooksTuplesCounter.WithLabelValues("resourcepermission", "update", "write").Add(float64(len(createOperations))) + } + } + } else { + b.logger.Debug("no tuples to update in zanzana", "namespace", newRP.Namespace) + } + }() + }, nil +} + +// AfterResourcePermissionDelete is a post-delete hook that removes the resource permission from Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionDelete(obj runtime.Object, _ *metav1.DeleteOptions) { + if b.zClient == nil { + return + } + + rp, ok := obj.(*iamv0.ResourcePermission) + if !ok { + b.logger.Error("failed to convert object to resourcePermission type", "object", obj) + return + } + + resourceType := "resourcepermission" + operation := "delete" + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) // Record wait time + + go func(rp *iamv0.ResourcePermission) { + start := time.Now() + status := "success" + + defer func() { + // Release the ticket after write is done + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc() + }() + + resource := rp.Spec.Resource + permissions := rp.Spec.Permissions + + // Generate delete tuples from the permissions + deleteOperations := make([]*v1.MutateOperation, 0, len(permissions)) + for _, p := range permissions { + deleteOperations = append(deleteOperations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: resource.ApiGroup, + Resource: resource.Resource, + Name: resource.Name, + }, + Permission: &v1.Permission{ + Kind: string(p.Kind), + Name: p.Name, + Verb: p.Verb, + }, + }, + }, + }) + } + + // Avoid writing if there are no valid tuples + if len(deleteOperations) == 0 { + return + } + + b.logger.Debug("deleting resource permission from zanzana", + "namespace", rp.Namespace, + "resource", resource, + "operationsCount", len(deleteOperations), + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: rp.Namespace, + Operations: deleteOperations, + }) + if err != nil { + status = "failure" + b.logger.Error("failed to delete resource permission from zanzana", + "err", err, + "namespace", rp.Namespace, + "resource", resource, + "operationsCount", len(deleteOperations), + ) + } else { + // Record successful tuple deletions + hooksTuplesCounter.WithLabelValues(resourceType, operation, "delete").Add(float64(len(deleteOperations))) + } + }(rp.DeepCopy()) // Pass a copy of the object +} diff --git a/pkg/registry/apis/iam/resource_permission_hooks_test.go b/pkg/registry/apis/iam/resource_permission_hooks_test.go new file mode 100644 index 00000000000..adb29a779a3 --- /dev/null +++ b/pkg/registry/apis/iam/resource_permission_hooks_test.go @@ -0,0 +1,514 @@ +package iam + +import ( + "context" + "sync" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stretchr/testify/require" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" +) + +type FakeZanzanaClient struct { + zanzana.Client + writeCallback func(context.Context, *v1.WriteRequest) error + readCallback func(context.Context, *v1.ReadRequest) (*v1.ReadResponse, error) + mutateCallback func(context.Context, *v1.MutateRequest) error +} + +// Read implements zanzana.Client. +func (f *FakeZanzanaClient) Read(ctx context.Context, req *v1.ReadRequest) (*v1.ReadResponse, error) { + if f.readCallback != nil { + return f.readCallback(ctx, req) + } + return &v1.ReadResponse{}, nil +} + +// Write implements zanzana.Client. +func (f *FakeZanzanaClient) Write(ctx context.Context, req *v1.WriteRequest) error { + return f.writeCallback(ctx, req) +} + +// Mutate implements zanzana.Client. +func (f *FakeZanzanaClient) Mutate(ctx context.Context, req *v1.MutateRequest) error { + if f.mutateCallback != nil { + return f.mutateCallback(ctx, req) + } + return nil +} + +func TestAfterResourcePermissionCreate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should create zanzana entries for folder resource permissions", func(t *testing.T) { + wg.Add(1) + folderPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-2", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"}, + {Kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "Edit"}, + }, + }, + } + + testFolderEntries := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) + require.Equal(t, "org-2", req.Namespace) + + expectedOperations := []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "u1", + Verb: "View", + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindBasicRole), + Name: "Editor", + Verb: "Edit", + }, + }, + }, + }, + } + require.ElementsMatch(t, expectedOperations, req.Operations) + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testFolderEntries} + b.AfterResourcePermissionCreate(&folderPerm, nil) + wg.Wait() + }) + + t.Run("should create zanzana entries for dashboard resource permissions", func(t *testing.T) { + wg.Add(1) + dashPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "View"}, + {Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "Edit"}, + }, + }, + } + + testDashEntries := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + // object := "resource:dashboard.grafana.app/dashboards/dash1" + + require.NotNil(t, req) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) + require.Equal(t, "default", req.Namespace) + + expectedOperations := []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindServiceAccount), + Name: "sa1", + Verb: "View", + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindTeam), + Name: "team1", + Verb: "Edit", + }, + }, + }, + }, + } + require.ElementsMatch(t, expectedOperations, req.Operations) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testDashEntries} + b.AfterResourcePermissionCreate(&dashPerm, nil) + wg.Wait() + }) +} + +func TestBeginResourcePermissionUpdate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should update zanzana entries for folder resource permissions", func(t *testing.T) { + wg.Add(1) + oldFolderPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-2", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"}, + }, + }, + } + + newFolderPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-2", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u2", Verb: "Edit"}, + {Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "View"}, + }, + }, + } + + testFolderWrite := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + + // Should delete old permission + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 3) + require.Equal(t, "org-2", req.Namespace) + + expectedOperations := []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "u1", + Verb: "View", + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindTeam), + Name: "team1", + Verb: "View", + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "u2", + Verb: "Edit", + }, + }, + }, + }, + } + + require.ElementsMatch(t, expectedOperations, req.Operations) + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testFolderWrite} + + // Call BeginUpdate which does all the work + finishFunc, err := b.BeginResourcePermissionUpdate(context.Background(), &newFolderPerm, &oldFolderPerm, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call the finish function with success=true to trigger the zanzana write + finishFunc(context.Background(), true) + }) + + wg.Wait() + t.Run("should update zanzana entries for dashboard resource permissions", func(t *testing.T) { + wg.Add(1) + oldDashPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"}, + }, + }, + } + + newDashPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "Edit"}, + }, + }, + } + + testDashWrite := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "default", req.Namespace) + + // Should delete old permission + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) + + expectedOperations := []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "u1", + Verb: "View", + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindServiceAccount), + Name: "sa1", + Verb: "Edit", + }, + }, + }, + }, + } + + require.ElementsMatch(t, expectedOperations, req.Operations) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testDashWrite} + + // Call BeginUpdate which does all the work + finishFunc, err := b.BeginResourcePermissionUpdate(context.Background(), &newDashPerm, &oldDashPerm, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call the finish function with success=true to trigger the zanzana write + finishFunc(context.Background(), true) + wg.Wait() + }) +} + +func TestAfterResourcePermissionDelete(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should delete zanzana entries for folder resource permissions", func(t *testing.T) { + wg.Add(1) + folderPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-2", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"}, + {Kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "Edit"}, + }, + }, + } + + testFolderDelete := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + + // Should have deletes but no writes + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) + + expectedOperations := []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "u1", + Verb: "View", + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindBasicRole), + Name: "Editor", + Verb: "Edit", + }, + }, + }, + }, + } + require.ElementsMatch(t, expectedOperations, req.Operations) + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testFolderDelete} + b.AfterResourcePermissionDelete(&folderPerm, nil) + wg.Wait() + }) + + t.Run("should delete zanzana entries for dashboard resource permissions", func(t *testing.T) { + wg.Add(1) + dashPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "View"}, + {Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "Edit"}, + }, + }, + } + + testDashDelete := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + + require.NotNil(t, req) + require.Equal(t, "default", req.Namespace) + + // Should have deletes but no writes + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) + + expectedOperations := []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindServiceAccount), + Name: "sa1", + Verb: "View", + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindTeam), + Name: "team1", + Verb: "Edit", + }, + }, + }, + }, + } + require.ElementsMatch(t, expectedOperations, req.Operations) + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testDashDelete} + b.AfterResourcePermissionDelete(&dashPerm, nil) + wg.Wait() + }) +} diff --git a/pkg/registry/apis/iam/role_hooks.go b/pkg/registry/apis/iam/role_hooks.go new file mode 100644 index 00000000000..831a2993fe5 --- /dev/null +++ b/pkg/registry/apis/iam/role_hooks.go @@ -0,0 +1,433 @@ +package iam + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic/registry" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/services/accesscontrol" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +// convertRolePermissionsToTuples converts role permissions (action/scope) to v1 TupleKey format +// using the shared zanzana.ConvertRolePermissionsToTuples utility and common.ToAuthzExtTupleKeys +func convertRolePermissionsToTuples(roleUID string, permissions []iamv0.CoreRolespecPermission) ([]*v1.TupleKey, error) { + // Convert IAM permissions to zanzana.RolePermission format + rolePerms := make([]zanzana.RolePermission, 0, len(permissions)) + for _, perm := range permissions { + // Split the scope to get kind, attribute, identifier + kind, _, identifier := accesscontrol.SplitScope(perm.Scope) + rolePerms = append(rolePerms, zanzana.RolePermission{ + Action: perm.Action, + Kind: kind, + Identifier: identifier, + }) + } + + // Translate to Zanzana tuples + openfgaTuples, err := zanzana.ConvertRolePermissionsToTuples(roleUID, rolePerms) + if err != nil { + return nil, err + } + + // Convert directly to v1 tuples using common utility + v1Tuples := common.ToAuthzExtTupleKeys(openfgaTuples) + + return v1Tuples, nil +} + +// AfterRoleCreate is a post-create hook that writes the role permissions to Zanzana (openFGA) +// It handles both Role and CoreRole types +func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object, _ *metav1.CreateOptions) { + if b.zClient == nil { + return + } + + var rType string + var rt *iamv0.CoreRole + + if coreRole, ok := obj.(*iamv0.CoreRole); ok { + rt = coreRole.DeepCopy() + rType = "coreRole" + } else if regRole, ok := obj.(*iamv0.Role); ok { + regRolePermissions := make([]iamv0.CoreRolespecPermission, len(regRole.Spec.Permissions)) + for i, p := range regRole.Spec.Permissions { + regRolePermissions[i] = iamv0.CoreRolespecPermission(p) + } + rt = &iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: regRole.Name, + Namespace: regRole.Namespace, + }, + Spec: iamv0.CoreRoleSpec{ + Permissions: regRolePermissions, + }, + } + rType = "role" + } else { + // Not a supported role type + return + } + + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(rType, "create").Observe(time.Since(wait).Seconds()) + + go func(role *iamv0.CoreRole, roleType string) { + start := time.Now() + status := "success" + defer func() { + <-b.zTickets + hooksDurationHistogram.WithLabelValues(rType, "create", status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues(rType, "create", status).Inc() + }() + + tuples, err := convertRolePermissionsToTuples(role.Name, role.Spec.Permissions) + if err != nil { + b.logger.Error("failed to convert role permissions to tuples", + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "err", err, + "permissionsCnt", len(role.Spec.Permissions), + ) + status = "failure" + return + } + + // Avoid writing if there are no valid tuples + if len(tuples) == 0 { + b.logger.Debug("no valid tuples to write for role", + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "permissionsCnt", len(role.Spec.Permissions), + ) + status = "failure" + return + } + + b.logger.Debug("writing role permissions to zanzana", + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "tuplesCnt", len(tuples), + "permissionsCnt", len(role.Spec.Permissions), + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err = b.zClient.Write(ctx, &v1.WriteRequest{ + Namespace: role.Namespace, + Writes: &v1.WriteRequestWrites{ + TupleKeys: tuples, + }, + }) + if err != nil { + b.logger.Error("failed to write role permissions to zanzana", + "err", err, + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "tuplesCnt", len(tuples), + ) + status = "failure" + return + } + + // Record successful tuple writes + hooksTuplesCounter.WithLabelValues(rType, "create", "write").Add(float64(len(tuples))) + }(rt.DeepCopy(), rType) +} + +// AfterRoleDelete is a post-delete hook that removes the role permissions from Zanzana (openFGA) +// It handles both Role and CoreRole types +func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object, _ *metav1.DeleteOptions) { + if b.zClient == nil { + return + } + + var rType string + var rt *iamv0.CoreRole + + // Try CoreRole first + if coreRole, ok := obj.(*iamv0.CoreRole); ok { + rt = coreRole.DeepCopy() + rType = "coreRole" + } else if regRole, ok := obj.(*iamv0.Role); ok { + regRolePermissions := make([]iamv0.CoreRolespecPermission, len(regRole.Spec.Permissions)) + for i, p := range regRole.Spec.Permissions { + regRolePermissions[i] = iamv0.CoreRolespecPermission(p) + } + rt = &iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: regRole.Name, + Namespace: regRole.Namespace, + }, + Spec: iamv0.CoreRoleSpec{ + Permissions: regRolePermissions, + }, + } + rType = "role" + } else { + // Not a supported role type + return + } + + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues("role", "delete").Observe(time.Since(wait).Seconds()) // Record wait time + + go func(role *iamv0.CoreRole, roleType string) { + defer func() { + <-b.zTickets + }() + + b.logger.Debug("deleting role permissions from zanzana", + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "permissionsCnt", len(role.Spec.Permissions), + ) + + tuples, err := convertRolePermissionsToTuples(role.Name, role.Spec.Permissions) + if err != nil { + b.logger.Error("failed to convert role permissions to tuples for deletion", + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "err", err, + "permissionsCnt", len(role.Spec.Permissions), + ) + return + } + + // Avoid deleting if there are no valid tuples + if len(tuples) == 0 { + b.logger.Debug("no valid tuples to delete for role", + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "permissionsCnt", len(role.Spec.Permissions), + ) + return + } + + // Convert tuples to TupleKeyWithoutCondition for deletion + deleteTuples := toTupleKeysWithoutCondition(tuples) + + b.logger.Debug("deleting role permissions from zanzana", + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "tuplesCnt", len(deleteTuples), + "permissionsCnt", len(role.Spec.Permissions), + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err = b.zClient.Write(ctx, &v1.WriteRequest{ + Namespace: role.Namespace, + Deletes: &v1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + }, + }) + if err != nil { + b.logger.Error("failed to delete role permissions from zanzana", + "err", err, + "namespace", role.Namespace, + "roleUID", role.Name, + "roleType", roleType, + "tuplesCnt", len(deleteTuples), + ) + } + }(rt.DeepCopy(), rType) +} + +// beginRoleUpdate is a pre-update hook that prepares zanzana updates +// It converts old and new permissions to tuples and performs the zanzana write after K8s update succeeds +// It handles both Role and CoreRole types +func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) { + if b.zClient == nil { + return nil, nil + } + var oldRole, newRole *iamv0.CoreRole + var roleType string + + if oldCoreRole, ok := oldObj.(*iamv0.CoreRole); ok { // Try CoreRole first + oldRole = oldCoreRole.DeepCopy() + newCoreRole, ok := obj.(*iamv0.CoreRole) + if !ok { + return nil, nil + } + newRole = newCoreRole.DeepCopy() + roleType = "coreRole" + } else if oldRegRole, ok := oldObj.(*iamv0.Role); ok { // Try Role + oldRegRolePermissions := make([]iamv0.CoreRolespecPermission, len(oldRegRole.Spec.Permissions)) + for i, p := range oldRegRole.Spec.Permissions { + oldRegRolePermissions[i] = iamv0.CoreRolespecPermission(p) + } + oldRole = &iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: oldRegRole.Name, + Namespace: oldRegRole.Namespace, + }, + Spec: iamv0.CoreRoleSpec{ + Permissions: oldRegRolePermissions, + }, + } + newRegRole, ok := obj.(*iamv0.Role) + if !ok { + return nil, nil + } + newRegRolePermissions := make([]iamv0.CoreRolespecPermission, len(newRegRole.Spec.Permissions)) + for i, p := range newRegRole.Spec.Permissions { + newRegRolePermissions[i] = iamv0.CoreRolespecPermission(p) + } + newRole = &iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: newRegRole.Name, + Namespace: newRegRole.Namespace, + }, + Spec: iamv0.CoreRoleSpec{ + Permissions: newRegRolePermissions, + }, + } + roleType = "role" + } else { + // Not a supported role type + return nil, nil + } + + // Return a finish function that performs the zanzana write only on success + return func(ctx context.Context, success bool) { + if !success { + // Update failed, don't write to zanzana + return + } + + // Grab a ticket to write to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(roleType, "update").Observe(time.Since(wait).Seconds()) // Record wait time + + go func(old *iamv0.CoreRole, new *iamv0.CoreRole) { + defer func() { + <-b.zTickets + }() + roleUID, namespace := old.Name, old.Namespace + oldPermissions, newPermissions := old.Spec.Permissions, new.Spec.Permissions + + // Convert old permissions to tuples for deletion + var oldTuples []*v1.TupleKey + if len(oldPermissions) > 0 { + var err error + oldTuples, err = convertRolePermissionsToTuples(roleUID, oldPermissions) + if err != nil { + b.logger.Error("failed to convert old role permissions to tuples", + "namespace", namespace, + "roleUID", roleUID, + "roleType", roleType, + "err", err, + ) + } + } + + // Convert new permissions to tuples for writing + newTuples, err := convertRolePermissionsToTuples(roleUID, newPermissions) + if err != nil { + b.logger.Error("failed to convert new role permissions to tuples", + "namespace", namespace, + "roleUID", roleUID, + "roleType", roleType, + "err", err, + ) + return + } + + b.logger.Debug("updating role permissions in zanzana", + "namespace", namespace, + "roleUID", roleUID, + "roleType", roleType, + "oldPermissionsCnt", len(oldPermissions), + "newPermissionsCnt", len(newPermissions), + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + // Prepare write request + req := &v1.WriteRequest{ + Namespace: namespace, + } + + // Add deletes for old tuples + if len(oldTuples) > 0 { + deleteTuples := toTupleKeysWithoutCondition(oldTuples) + req.Deletes = &v1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + } + b.logger.Debug("deleting existing role permissions from zanzana", + "namespace", namespace, + "roleUID", roleUID, + "roleType", roleType, + "tuplesCnt", len(deleteTuples), + ) + } + + // Add writes for new tuples + if len(newTuples) > 0 { + req.Writes = &v1.WriteRequestWrites{ + TupleKeys: newTuples, + } + b.logger.Debug("writing new role permissions to zanzana", + "namespace", namespace, + "roleUID", roleUID, + "roleType", roleType, + "tuplesCnt", len(newTuples), + ) + } + + // Only make the request if there are deletes or writes + if req.Deletes != nil || req.Writes != nil { + err = b.zClient.Write(ctx, req) + if err != nil { + b.logger.Error("failed to update role permissions in zanzana", + "err", err, + "namespace", namespace, + "roleUID", roleUID, + "roleType", roleType, + ) + } + } + }(oldRole.DeepCopy(), newRole.DeepCopy()) + }, nil +} + +// tupleToTupleKeyWithoutCondition converts a TupleKey to TupleKeyWithoutCondition +// This is needed for delete operations which don't support conditions +func tupleToTupleKeyWithoutCondition(tuple *v1.TupleKey) *v1.TupleKeyWithoutCondition { + return &v1.TupleKeyWithoutCondition{ + User: tuple.User, + Relation: tuple.Relation, + Object: tuple.Object, + } +} + +// toTupleKeysWithoutCondition converts v1.TupleKey to v1.TupleKeyWithoutCondition +// by stripping the condition field, which is required for delete operations +func toTupleKeysWithoutCondition(tuples []*v1.TupleKey) []*v1.TupleKeyWithoutCondition { + result := make([]*v1.TupleKeyWithoutCondition, len(tuples)) + for i, t := range tuples { + result[i] = tupleToTupleKeyWithoutCondition(t) + } + return result +} diff --git a/pkg/registry/apis/iam/role_hooks_test.go b/pkg/registry/apis/iam/role_hooks_test.go new file mode 100644 index 00000000000..497150f1c61 --- /dev/null +++ b/pkg/registry/apis/iam/role_hooks_test.go @@ -0,0 +1,989 @@ +package iam + +import ( + "context" + "sync" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stretchr/testify/require" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +func requireTuplesMatch(t *testing.T, actual []*v1.TupleKey, expected []*v1.TupleKey, msgAndArgs ...interface{}) { + t.Helper() + for _, exp := range expected { + found := false + for _, act := range actual { + if act.User == exp.User && + act.Relation == exp.Relation && + act.Object == exp.Object { + found = true + break + } + } + if !found { + require.Fail(t, "Expected tuple not found", "Tuple: %+v\n%v", exp, msgAndArgs) + } + } +} + +func requireDeleteTuplesMatch(t *testing.T, actual []*v1.TupleKeyWithoutCondition, expected []*v1.TupleKeyWithoutCondition, msgAndArgs ...interface{}) { + t.Helper() + for _, exp := range expected { + found := false + for _, act := range actual { + if act.User == exp.User && + act.Relation == exp.Relation && + act.Object == exp.Object { + found = true + break + } + } + if !found { + require.Fail(t, "Expected delete tuple not found", "Tuple: %+v\n%v", exp, msgAndArgs) + } + } +} + +func TestAfterCoreRoleCreate(t *testing.T) { + var wg sync.WaitGroup + + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should create zanzana entries for core role with folder permissions", func(t *testing.T) { + wg.Add(1) + coreRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Test Role", + Description: "Test role for folders", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "folders:write", Scope: "folders:uid:folder1"}, + }, + }, + } + + testCoreRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + require.Equal(t, "org-1", req.Namespace) + + expectedTuples := []*v1.TupleKey{ + {User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder1"}, + {User: "role:test-role-uid#assignee", Relation: "update", Object: "folder:folder1"}, + } + + requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples) + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleEntries} + b.AfterRoleCreate(&coreRole, nil) + wg.Wait() + }) + + t.Run("should create zanzana entries for core role with dashboard permissions", func(t *testing.T) { + wg.Add(1) + coreRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dashboard-role-uid", + Namespace: "default", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Dashboard Role", + Description: "Test role for dashboards", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "dashboards:read", Scope: "dashboards:uid:dash1"}, + {Action: "dashboards:write", Scope: "dashboards:uid:dash1"}, + }, + }, + } + + testDashboardRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + require.Equal(t, "default", req.Namespace) + + // Check subject is role with assignee relation + for _, tuple := range req.Writes.TupleKeys { + require.Equal(t, "role:dashboard-role-uid#assignee", tuple.User) + require.Contains(t, tuple.Object, "resource:") + require.Contains(t, tuple.Object, "dashboard") + } + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleEntries} + b.AfterRoleCreate(&coreRole, nil) + wg.Wait() + }) + + t.Run("should handle wildcard scopes", func(t *testing.T) { + wg.Add(1) + coreRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wildcard-role-uid", + Namespace: "org-2", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Wildcard Role", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:*"}, + }, + }, + } + + testWildcardEntries := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 1) + + tuple := req.Writes.TupleKeys[0] + require.Equal(t, "role:wildcard-role-uid#assignee", tuple.User) + // Wildcard should create a group_resource tuple + require.Contains(t, tuple.Object, "group_resource:") + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testWildcardEntries} + b.AfterRoleCreate(&coreRole, nil) + wg.Wait() + }) + + t.Run("should skip untranslatable permissions", func(t *testing.T) { + wg.Add(1) + coreRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Mixed Role", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "unknown:action", Scope: "unknown:scope"}, // This should be skipped + }, + }, + } + + testMixedEntries := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + // Should only have 1 tuple (the untranslatable one should be skipped) + require.Len(t, req.Writes.TupleKeys, 1) + + tuple := req.Writes.TupleKeys[0] + require.Equal(t, "role:mixed-role-uid#assignee", tuple.User) + require.Equal(t, "folder:folder1", tuple.Object) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testMixedEntries} + b.AfterRoleCreate(&coreRole, nil) + wg.Wait() + }) +} + +func TestAfterRoleCreate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should create zanzana entries for role with folder permissions", func(t *testing.T) { + wg.Add(1) + role := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "custom-role-uid", + Namespace: "org-3", + }, + Spec: iamv0.RoleSpec{ + Title: "Custom Role", + Description: "Custom role for folders", + Permissions: []iamv0.RolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder2"}, + {Action: "folders:delete", Scope: "folders:uid:folder2"}, + }, + }, + } + + testRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + require.Equal(t, "org-3", req.Namespace) + + expectedTuples := []*v1.TupleKey{ + {User: "role:custom-role-uid#assignee", Relation: "get", Object: "folder:folder2"}, + {User: "role:custom-role-uid#assignee", Relation: "delete", Object: "folder:folder2"}, + } + + requireTuplesMatch(t, req.Writes.TupleKeys, expectedTuples) + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testRoleEntries} + b.AfterRoleCreate(&role, nil) + wg.Wait() + }) + + t.Run("should create zanzana entries for role with dashboard permissions", func(t *testing.T) { + wg.Add(1) + role := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dash-role-uid", + Namespace: "default", + }, + Spec: iamv0.RoleSpec{ + Title: "Dashboard Custom Role", + Description: "Custom role for dashboards", + Permissions: []iamv0.RolespecPermission{ + {Action: "dashboards:read", Scope: "dashboards:uid:mydash"}, + {Action: "dashboards:delete", Scope: "dashboards:uid:mydash"}, + }, + }, + } + + testDashRoleEntries := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + require.Equal(t, "default", req.Namespace) + + // Check subject is role with assignee relation + for _, tuple := range req.Writes.TupleKeys { + require.Equal(t, "role:dash-role-uid#assignee", tuple.User) + require.Contains(t, tuple.Object, "resource:") + } + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testDashRoleEntries} + b.AfterRoleCreate(&role, nil) + wg.Wait() + }) + + t.Run("should merge folder resource tuples with same object and user", func(t *testing.T) { + wg.Add(1) + role := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "merge-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.RoleSpec{ + Title: "Merge Test Role", + Permissions: []iamv0.RolespecPermission{ + // These should create folder resource tuples that get merged + {Action: "dashboards:read", Scope: "folders:uid:parent-folder"}, + {Action: "dashboards:write", Scope: "folders:uid:parent-folder"}, + }, + }, + } + + testMergedEntries := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + // After merging, we should have tuples for the folder resource actions + require.Greater(t, len(req.Writes.TupleKeys), 0) + + for _, tuple := range req.Writes.TupleKeys { + require.Equal(t, "role:merge-role-uid#assignee", tuple.User) + } + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testMergedEntries} + b.AfterRoleCreate(&role, nil) + wg.Wait() + }) +} + +func TestBeginCoreRoleUpdate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should update zanzana entries when permissions change", func(t *testing.T) { + wg.Add(1) + oldRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Test Role", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "folders:write", Scope: "folders:uid:folder1"}, + }, + }, + } + + newRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Test Role Updated", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder2"}, + {Action: "folders:delete", Scope: "folders:uid:folder2"}, + }, + }, + } + + testUpdate := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + + // Verify deletes (old permissions) + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 2) + + expectedDeletes := []*v1.TupleKeyWithoutCondition{ + {User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder1"}, + {User: "role:test-role-uid#assignee", Relation: "update", Object: "folder:folder1"}, + } + requireDeleteTuplesMatch(t, req.Deletes.TupleKeys, expectedDeletes) + + // Verify writes (new permissions) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + + expectedWrites := []*v1.TupleKey{ + {User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder2"}, + {User: "role:test-role-uid#assignee", Relation: "delete", Object: "folder:folder2"}, + } + requireTuplesMatch(t, req.Writes.TupleKeys, expectedWrites) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testUpdate} + + // Call BeginUpdate which does all the work + finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call the finish function with success=true to trigger the zanzana write + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should handle adding new permissions", func(t *testing.T) { + wg.Add(1) + oldRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "expand-role-uid", + Namespace: "org-2", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Expand Role", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + }, + }, + } + + newRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "expand-role-uid", + Namespace: "org-2", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Expand Role", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "folders:write", Scope: "folders:uid:folder1"}, + {Action: "folders:delete", Scope: "folders:uid:folder1"}, + }, + }, + } + + testExpand := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + + // Should delete old permission + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 1) + + // Should write all new permissions + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 3) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testExpand} + + // Call BeginUpdate which does all the work + finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call the finish function with success=true to trigger the zanzana write + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should handle removing all permissions", func(t *testing.T) { + wg.Add(1) + oldRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clear-role-uid", + Namespace: "org-3", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Clear Role", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "folders:write", Scope: "folders:uid:folder1"}, + }, + }, + } + + newRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clear-role-uid", + Namespace: "org-3", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Clear Role", + Permissions: []iamv0.CoreRolespecPermission{}, + }, + } + + testClear := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-3", req.Namespace) + + // Should delete old permissions + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 2) + + // Should have no writes + require.Nil(t, req.Writes) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testClear} + + // Call BeginUpdate which does all the work + finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call the finish function with success=true to trigger the zanzana write + finishFunc(context.Background(), true) + wg.Wait() + }) +} + +func TestBeginRoleUpdate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should update zanzana entries when permissions change", func(t *testing.T) { + wg.Add(1) + oldRole := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "custom-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.RoleSpec{ + Title: "Custom Role", + Permissions: []iamv0.RolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "folders:write", Scope: "folders:uid:folder1"}, + }, + }, + } + + newRole := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "custom-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.RoleSpec{ + Title: "Custom Role Updated", + Permissions: []iamv0.RolespecPermission{ + {Action: "dashboards:read", Scope: "dashboards:uid:dash1"}, + {Action: "dashboards:write", Scope: "dashboards:uid:dash1"}, + }, + }, + } + + testUpdate := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + + // Verify deletes (old permissions) + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 2) + + expectedDeletes := []*v1.TupleKeyWithoutCondition{ + {User: "role:custom-role-uid#assignee", Relation: "get", Object: "folder:folder1"}, + {User: "role:custom-role-uid#assignee", Relation: "update", Object: "folder:folder1"}, + } + requireDeleteTuplesMatch(t, req.Deletes.TupleKeys, expectedDeletes) + + // Verify writes (new permissions) - dashboards use resource type + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + + // All writes should be for dashboards + for _, tuple := range req.Writes.TupleKeys { + require.Equal(t, "role:custom-role-uid#assignee", tuple.User) + require.Contains(t, tuple.Object, "resource:") + require.Contains(t, tuple.Object, "dashboard") + } + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testUpdate} + + // Call BeginUpdate which does all the work + finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call the finish function with success=true to trigger the zanzana write + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should handle completely new permission set", func(t *testing.T) { + wg.Add(1) + oldRole := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "swap-role-uid", + Namespace: "default", + }, + Spec: iamv0.RoleSpec{ + Title: "Swap Role", + Permissions: []iamv0.RolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + }, + }, + } + + newRole := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "swap-role-uid", + Namespace: "default", + }, + Spec: iamv0.RoleSpec{ + Title: "Swap Role", + Permissions: []iamv0.RolespecPermission{ + {Action: "folders:write", Scope: "folders:uid:folder2"}, + {Action: "folders:delete", Scope: "folders:uid:folder2"}, + }, + }, + } + + testSwap := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "default", req.Namespace) + + // Should delete old permission + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 1) + require.Equal(t, "role:swap-role-uid#assignee", req.Deletes.TupleKeys[0].User) + require.Equal(t, "folder:folder1", req.Deletes.TupleKeys[0].Object) + + // Should write new permissions + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + for _, tuple := range req.Writes.TupleKeys { + require.Equal(t, "role:swap-role-uid#assignee", tuple.User) + require.Equal(t, "folder:folder2", tuple.Object) + } + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testSwap} + + // Call BeginUpdate which does all the work + finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call the finish function with success=true to trigger the zanzana write + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should handle adding permissions to empty role", func(t *testing.T) { + wg.Add(1) + oldRole := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-role-uid", + Namespace: "org-2", + }, + Spec: iamv0.RoleSpec{ + Title: "Empty Role", + Permissions: []iamv0.RolespecPermission{}, + }, + } + + newRole := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-role-uid", + Namespace: "org-2", + }, + Spec: iamv0.RoleSpec{ + Title: "Empty Role", + Permissions: []iamv0.RolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + }, + }, + } + + testAddToEmpty := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + + // Should have no deletes + require.Nil(t, req.Deletes) + + // Should write new permission + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 1) + require.Equal(t, "role:empty-role-uid#assignee", req.Writes.TupleKeys[0].User) + require.Equal(t, "folder:folder1", req.Writes.TupleKeys[0].Object) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testAddToEmpty} + + // Call BeginUpdate which does all the work + finishFunc, err := b.BeginRoleUpdate(context.Background(), &newRole, &oldRole, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call the finish function with success=true to trigger the zanzana write + finishFunc(context.Background(), true) + wg.Wait() + }) +} + +func TestAfterCoreRoleDelete(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should delete zanzana entries for core role with folder permissions", func(t *testing.T) { + wg.Add(1) + coreRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Test Role", + Description: "Test role for folders", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "folders:write", Scope: "folders:uid:folder1"}, + }, + }, + } + + testCoreRoleDeletes := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 2) + require.Equal(t, "org-1", req.Namespace) + + expectedDeletes := []*v1.TupleKeyWithoutCondition{ + {User: "role:test-role-uid#assignee", Relation: "get", Object: "folder:folder1"}, + {User: "role:test-role-uid#assignee", Relation: "update", Object: "folder:folder1"}, + } + + requireDeleteTuplesMatch(t, req.Deletes.TupleKeys, expectedDeletes) + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testCoreRoleDeletes} + b.AfterRoleDelete(&coreRole, nil) + wg.Wait() + }) + + t.Run("should delete zanzana entries for core role with dashboard permissions", func(t *testing.T) { + wg.Add(1) + coreRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dashboard-role-uid", + Namespace: "default", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Dashboard Role", + Description: "Test role for dashboards", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "dashboards:read", Scope: "dashboards:uid:dash1"}, + {Action: "dashboards:write", Scope: "dashboards:uid:dash1"}, + }, + }, + } + + testDashboardRoleDeletes := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 2) + require.Equal(t, "default", req.Namespace) + + // Check all deletes have the correct subject + for _, tuple := range req.Deletes.TupleKeys { + require.Equal(t, "role:dashboard-role-uid#assignee", tuple.User) + require.Contains(t, tuple.Object, "resource:") + } + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testDashboardRoleDeletes} + b.AfterRoleDelete(&coreRole, nil) + wg.Wait() + }) + + t.Run("should handle wildcard scopes on delete", func(t *testing.T) { + wg.Add(1) + coreRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wildcard-role-uid", + Namespace: "org-2", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Wildcard Role", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:*"}, + }, + }, + } + + testWildcardDeletes := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 1) + + tuple := req.Deletes.TupleKeys[0] + require.Equal(t, "role:wildcard-role-uid#assignee", tuple.User) + require.Contains(t, tuple.Object, "group_resource:") + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testWildcardDeletes} + b.AfterRoleDelete(&coreRole, nil) + wg.Wait() + }) + + t.Run("should skip untranslatable permissions on delete", func(t *testing.T) { + wg.Add(1) + coreRole := iamv0.CoreRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.CoreRoleSpec{ + Title: "Mixed Role", + Permissions: []iamv0.CoreRolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "unknown:action", Scope: "unknown:scope"}, // This should be skipped + }, + }, + } + + testMixedDeletes := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Deletes) + // Should only delete 1 tuple (the untranslatable one should be skipped) + require.Len(t, req.Deletes.TupleKeys, 1) + + tuple := req.Deletes.TupleKeys[0] + require.Equal(t, "role:mixed-role-uid#assignee", tuple.User) + require.Equal(t, "folder:folder1", tuple.Object) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testMixedDeletes} + b.AfterRoleDelete(&coreRole, nil) + wg.Wait() + }) +} + +func TestAfterRoleDelete(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + t.Run("should delete zanzana entries for role with folder permissions", func(t *testing.T) { + wg.Add(1) + role := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "custom-role-uid", + Namespace: "org-3", + }, + Spec: iamv0.RoleSpec{ + Title: "Custom Role", + Description: "Custom role for folders", + Permissions: []iamv0.RolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder2"}, + {Action: "folders:delete", Scope: "folders:uid:folder2"}, + }, + }, + } + + testRoleDeletes := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 2) + require.Equal(t, "org-3", req.Namespace) + + expectedDeletes := []*v1.TupleKeyWithoutCondition{ + {User: "role:custom-role-uid#assignee", Relation: "get", Object: "folder:folder2"}, + {User: "role:custom-role-uid#assignee", Relation: "delete", Object: "folder:folder2"}, + } + + requireDeleteTuplesMatch(t, req.Deletes.TupleKeys, expectedDeletes) + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testRoleDeletes} + b.AfterRoleDelete(&role, nil) + wg.Wait() + }) + + t.Run("should delete zanzana entries for role with dashboard permissions", func(t *testing.T) { + wg.Add(1) + role := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dash-role-uid", + Namespace: "default", + }, + Spec: iamv0.RoleSpec{ + Title: "Dashboard Custom Role", + Description: "Custom role for dashboards", + Permissions: []iamv0.RolespecPermission{ + {Action: "dashboards:read", Scope: "dashboards:uid:mydash"}, + {Action: "dashboards:delete", Scope: "dashboards:uid:mydash"}, + }, + }, + } + + testDashRoleDeletes := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 2) + require.Equal(t, "default", req.Namespace) + + // Check all deletes have the correct subject + for _, tuple := range req.Deletes.TupleKeys { + require.Equal(t, "role:dash-role-uid#assignee", tuple.User) + require.Contains(t, tuple.Object, "resource:") + } + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testDashRoleDeletes} + b.AfterRoleDelete(&role, nil) + wg.Wait() + }) + + t.Run("should handle multiple permissions on delete", func(t *testing.T) { + wg.Add(1) + role := iamv0.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-role-uid", + Namespace: "org-1", + }, + Spec: iamv0.RoleSpec{ + Title: "Multi Permission Role", + Permissions: []iamv0.RolespecPermission{ + {Action: "folders:read", Scope: "folders:uid:folder1"}, + {Action: "folders:write", Scope: "folders:uid:folder1"}, + {Action: "folders:delete", Scope: "folders:uid:folder1"}, + }, + }, + } + + testMultiDeletes := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 3) + + // All should be for the same role and folder + for _, tuple := range req.Deletes.TupleKeys { + require.Equal(t, "role:multi-role-uid#assignee", tuple.User) + require.Equal(t, "folder:folder1", tuple.Object) + } + + // Check all expected relations are present + relations := make(map[string]bool) + for _, tuple := range req.Deletes.TupleKeys { + relations[tuple.Relation] = true + } + require.True(t, relations["get"], "Expected 'get' relation") + require.True(t, relations["update"], "Expected 'update' relation") + require.True(t, relations["delete"], "Expected 'delete' relation") + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testMultiDeletes} + b.AfterRoleDelete(&role, nil) + wg.Wait() + }) +} diff --git a/pkg/registry/apis/iam/team_binding_hooks.go b/pkg/registry/apis/iam/team_binding_hooks.go new file mode 100644 index 00000000000..dcad58cae0a --- /dev/null +++ b/pkg/registry/apis/iam/team_binding_hooks.go @@ -0,0 +1,363 @@ +package iam + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic/registry" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" +) + +// convertTeamBindingToTuple converts a TeamBinding to a v1 TupleKey format +// TeamBinding represents a user's membership in a team with a specific permission level +func convertTeamBindingToTuple(tb *iamv0.TeamBinding) (*v1.TupleKey, error) { + if tb.Spec.Subject.Name == "" { + return nil, errEmptyName + } + + if tb.Spec.TeamRef.Name == "" { + return nil, errEmptyName + } + + // Map permission to relation + var relation string + switch tb.Spec.Permission { + case iamv0.TeamBindingTeamPermissionAdmin: + relation = zanzana.RelationTeamAdmin + case iamv0.TeamBindingTeamPermissionMember: + relation = zanzana.RelationTeamMember + default: + // Default to member if unknown permission + relation = zanzana.RelationTeamMember + } + + // Create tuple: user:{subjectUID} has {relation} relation to team:{teamUID} + tuple := &v1.TupleKey{ + User: zanzana.NewTupleEntry(zanzana.TypeUser, tb.Spec.Subject.Name, ""), + Relation: relation, + Object: zanzana.NewTupleEntry(zanzana.TypeTeam, tb.Spec.TeamRef.Name, ""), + } + + return tuple, nil +} + +// AfterTeamBindingCreate is a post-create hook that writes the team binding to Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingCreate(obj runtime.Object, _ *metav1.CreateOptions) { + if b.zClient == nil { + return + } + + tb, ok := obj.(*iamv0.TeamBinding) + if !ok { + b.logger.Error("failed to convert object to TeamBinding type", "object", obj) + return + } + + resourceType := "teambinding" + operation := "create" + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) + + go func(tb *iamv0.TeamBinding) { + start := time.Now() + status := "success" + + defer func() { + // Release the ticket after write is done + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc() + }() + + tuple, err := convertTeamBindingToTuple(tb) + if err != nil { + b.logger.Error("failed to convert team binding to tuple", + "namespace", tb.Namespace, + "name", tb.Name, + "subject", tb.Spec.Subject.Name, + "teamRef", tb.Spec.TeamRef.Name, + "permission", tb.Spec.Permission, + "err", err, + ) + status = "failure" + return + } + + b.logger.Debug("writing team binding to zanzana", + "namespace", tb.Namespace, + "name", tb.Name, + "subject", tb.Spec.Subject.Name, + "teamRef", tb.Spec.TeamRef.Name, + "permission", tb.Spec.Permission, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err = b.zClient.Write(ctx, &v1.WriteRequest{ + Namespace: tb.Namespace, + Writes: &v1.WriteRequestWrites{ + TupleKeys: []*v1.TupleKey{tuple}, + }, + }) + if err != nil { + status = "failure" + b.logger.Error("failed to write team binding to zanzana", + "err", err, + "namespace", tb.Namespace, + "name", tb.Name, + "subject", tb.Spec.Subject.Name, + "teamRef", tb.Spec.TeamRef.Name, + "permission", tb.Spec.Permission, + ) + } else { + // Record successful tuple write + hooksTuplesCounter.WithLabelValues(resourceType, operation, "write").Inc() + } + }(tb.DeepCopy()) // Pass a copy of the object +} + +// BeginTeamBindingUpdate is a pre-update hook that prepares zanzana updates +// It converts old and new team bindings to tuples and performs the zanzana write after K8s update succeeds +func (b *IdentityAccessManagementAPIBuilder) BeginTeamBindingUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) { + if b.zClient == nil { + return nil, nil + } + + // Extract team bindings from both old and new objects + oldTB, ok := oldObj.(*iamv0.TeamBinding) + if !ok { + return nil, nil + } + + newTB, ok := obj.(*iamv0.TeamBinding) + if !ok { + return nil, nil + } + + if oldTB.Spec.Subject.Name == newTB.Spec.Subject.Name && oldTB.Spec.TeamRef.Name == newTB.Spec.TeamRef.Name && oldTB.Spec.Permission == newTB.Spec.Permission { + return nil, nil // No changes to the team binding + } + + if newTB.Spec.Subject.Name == "" || newTB.Spec.TeamRef.Name == "" { + b.logger.Error("invalid team binding", + "namespace", newTB.Namespace, + "name", newTB.Name, + "subject", newTB.Spec.Subject.Name, + "teamRef", newTB.Spec.TeamRef.Name, + ) + return nil, nil + } + + // Convert old team binding to tuple for deletion + var oldTuple *v1.TupleKey + var oldErr error + if oldTB.Spec.Subject.Name != "" && oldTB.Spec.TeamRef.Name != "" { + oldTuple, oldErr = convertTeamBindingToTuple(oldTB) + if oldErr != nil { + b.logger.Error("failed to convert old team binding to tuple", + "namespace", oldTB.Namespace, + "name", oldTB.Name, + "err", oldErr, + ) + return nil, nil + } + } + + // Convert new team binding to tuple for writing + var newTuple *v1.TupleKey + var newErr error + if newTB.Spec.Subject.Name != "" && newTB.Spec.TeamRef.Name != "" { + newTuple, newErr = convertTeamBindingToTuple(newTB) + if newErr != nil { + b.logger.Error("failed to convert new team binding to tuple", + "namespace", newTB.Namespace, + "name", newTB.Name, + "err", newErr, + ) + return nil, nil + } + } + + // Return a finish function that performs the zanzana write only on success + return func(ctx context.Context, success bool) { + if !success { + return + } + + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues("teambinding", "update").Observe(time.Since(wait).Seconds()) + + go func() { + start := time.Now() + status := "success" + + defer func() { + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues("teambinding", "update", status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues("teambinding", "update", status).Inc() + }() + + b.logger.Debug("updating team binding in zanzana", + "namespace", newTB.Namespace, + "name", newTB.Name, + "oldSubject", oldTB.Spec.Subject.Name, + "newSubject", newTB.Spec.Subject.Name, + "oldTeamRef", oldTB.Spec.TeamRef.Name, + "newTeamRef", newTB.Spec.TeamRef.Name, + "oldPermission", oldTB.Spec.Permission, + "newPermission", newTB.Spec.Permission, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + // Prepare write request + req := &v1.WriteRequest{ + Namespace: newTB.Namespace, + } + + // Add delete for old tuple + if oldTuple != nil && oldErr == nil { + deleteTuple := toTupleKeysWithoutCondition([]*v1.TupleKey{oldTuple}) + req.Deletes = &v1.WriteRequestDeletes{ + TupleKeys: deleteTuple, + } + b.logger.Debug("deleting existing team binding from zanzana", + "namespace", newTB.Namespace, + "subject", oldTB.Spec.Subject.Name, + "teamRef", oldTB.Spec.TeamRef.Name, + ) + } + + // Add write for new tuple + if newTuple != nil && newErr == nil { + req.Writes = &v1.WriteRequestWrites{ + TupleKeys: []*v1.TupleKey{newTuple}, + } + b.logger.Debug("writing new team binding to zanzana", + "namespace", newTB.Namespace, + "subject", newTB.Spec.Subject.Name, + "teamRef", newTB.Spec.TeamRef.Name, + ) + } + + // Only make the request if there are deletes or writes + if (req.Deletes != nil && len(req.Deletes.TupleKeys) > 0) || (req.Writes != nil && len(req.Writes.TupleKeys) > 0) { + err := b.zClient.Write(ctx, req) + if err != nil { + status = "failure" + b.logger.Error("failed to update team binding in zanzana", + "err", err, + "namespace", newTB.Namespace, + "name", newTB.Name, + ) + } else { + // Record successful tuple operations + if oldTuple != nil && oldErr == nil { + hooksTuplesCounter.WithLabelValues("teambinding", "update", "delete").Inc() + } + if newTuple != nil && newErr == nil { + hooksTuplesCounter.WithLabelValues("teambinding", "update", "write").Inc() + } + } + } else { + b.logger.Debug("no tuples to update in zanzana", "namespace", newTB.Namespace, "name", newTB.Name) + } + }() + }, nil +} + +// AfterTeamBindingDelete is a post-delete hook that removes the team binding from Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingDelete(obj runtime.Object, _ *metav1.DeleteOptions) { + if b.zClient == nil { + return + } + + tb, ok := obj.(*iamv0.TeamBinding) + if !ok { + b.logger.Error("failed to convert object to TeamBinding type", "object", obj) + return + } + + resourceType := "teambinding" + operation := "delete" + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) + + go func(tb *iamv0.TeamBinding) { + start := time.Now() + status := "success" + + defer func() { + // Release the ticket after write is done + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc() + }() + + tuple, err := convertTeamBindingToTuple(tb) + if err != nil { + b.logger.Error("failed to convert team binding to tuple for deletion", + "namespace", tb.Namespace, + "name", tb.Name, + "subject", tb.Spec.Subject.Name, + "teamRef", tb.Spec.TeamRef.Name, + "err", err, + ) + status = "failure" + return + } + + // Convert tuple to TupleKeyWithoutCondition for deletion + deleteTuple := toTupleKeysWithoutCondition([]*v1.TupleKey{tuple}) + + b.logger.Debug("deleting team binding from zanzana", + "namespace", tb.Namespace, + "name", tb.Name, + "subject", tb.Spec.Subject.Name, + "teamRef", tb.Spec.TeamRef.Name, + "permission", tb.Spec.Permission, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err = b.zClient.Write(ctx, &v1.WriteRequest{ + Namespace: tb.Namespace, + Deletes: &v1.WriteRequestDeletes{ + TupleKeys: deleteTuple, + }, + }) + if err != nil { + status = "failure" + b.logger.Error("failed to delete team binding from zanzana", + "err", err, + "namespace", tb.Namespace, + "name", tb.Name, + "subject", tb.Spec.Subject.Name, + "teamRef", tb.Spec.TeamRef.Name, + ) + } else { + // Record successful tuple deletion + hooksTuplesCounter.WithLabelValues(resourceType, operation, "delete").Inc() + } + }(tb.DeepCopy()) // Pass a copy of the object +} diff --git a/pkg/registry/apis/iam/team_binding_hooks_test.go b/pkg/registry/apis/iam/team_binding_hooks_test.go new file mode 100644 index 00000000000..2f56ba12c87 --- /dev/null +++ b/pkg/registry/apis/iam/team_binding_hooks_test.go @@ -0,0 +1,966 @@ +package iam + +import ( + "context" + "sync" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/stretchr/testify/require" +) + +func TestAfterTeamBindingCreate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should create zanzana entry for team binding with member permission", func(t *testing.T) { + wg.Add(1) + teamBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + External: false, + }, + } + + testMemberBinding := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 1) + require.Equal(t, "org-1", req.Namespace) + require.Nil(t, req.Deletes) + + expectedTuple := &v1.TupleKey{ + User: "user:user-1", + Relation: "member", + Object: "team:team-1", + } + + actualTuple := req.Writes.TupleKeys[0] + require.Equal(t, expectedTuple.User, actualTuple.User) + require.Equal(t, expectedTuple.Relation, actualTuple.Relation) + require.Equal(t, expectedTuple.Object, actualTuple.Object) + require.Nil(t, actualTuple.Condition) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testMemberBinding} + b.AfterTeamBindingCreate(&teamBinding, nil) + wg.Wait() + }) + + t.Run("should create zanzana entry for team binding with admin permission", func(t *testing.T) { + wg.Add(1) + teamBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-2", + Namespace: "org-2", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-2", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-2", + }, + Permission: iamv0.TeamBindingTeamPermissionAdmin, + External: true, + }, + } + + testAdminBinding := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 1) + require.Equal(t, "org-2", req.Namespace) + require.Nil(t, req.Deletes) + + expectedTuple := &v1.TupleKey{ + User: "user:user-2", + Relation: "admin", + Object: "team:team-2", + } + + actualTuple := req.Writes.TupleKeys[0] + require.Equal(t, expectedTuple.User, actualTuple.User) + require.Equal(t, expectedTuple.Relation, actualTuple.Relation) + require.Equal(t, expectedTuple.Object, actualTuple.Object) + require.Nil(t, actualTuple.Condition) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testAdminBinding} + b.AfterTeamBindingCreate(&teamBinding, nil) + wg.Wait() + }) + + t.Run("should not write to zanzana when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + teamBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-3", + Namespace: "org-3", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-3", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-3", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + // Should not panic or error when zClient is nil + builder.AfterTeamBindingCreate(&teamBinding, nil) + }) + + t.Run("should handle conversion error gracefully", func(t *testing.T) { + // TeamBinding with empty subject name should fail conversion + teamBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-4", + Namespace: "org-4", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "", // Empty name should cause error + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-4", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + writeCalled := false + testErrorHandling := func(ctx context.Context, req *v1.WriteRequest) error { + writeCalled = true + // Should not be called due to conversion error + require.Fail(t, "Write should not be called when conversion fails") + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testErrorHandling} + b.AfterTeamBindingCreate(&teamBinding, nil) + // Wait a bit to ensure the goroutine has time to process + // The goroutine will complete but won't call the write callback + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when conversion fails") + }) +} + +func TestBeginTeamBindingUpdate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should update zanzana entry when permission changes from member to admin", func(t *testing.T) { + wg.Add(1) + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionAdmin, + }, + } + + testPermissionUpdate := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + + // Should delete old member permission + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 1) + require.Equal( + t, + req.Deletes.TupleKeys[0], + &v1.TupleKeyWithoutCondition{User: "user:user-1", Relation: "member", Object: "team:team-1"}, + ) + + // Should write new admin permission + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 1) + require.Equal( + t, + req.Writes.TupleKeys[0], + &v1.TupleKey{User: "user:user-1", Relation: "admin", Object: "team:team-1"}, + ) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testPermissionUpdate} + + finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should update zanzana entry when user changes", func(t *testing.T) { + wg.Add(1) + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-2", + Namespace: "org-2", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-2", + Namespace: "org-2", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-2", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + testUserUpdate := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + + // Should delete old user binding + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 1) + require.Equal( + t, + req.Deletes.TupleKeys[0], + &v1.TupleKeyWithoutCondition{User: "user:user-1", Relation: "member", Object: "team:team-1"}, + ) + + // Should write new user binding + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 1) + require.Equal( + t, + req.Writes.TupleKeys[0], + &v1.TupleKey{User: "user:user-2", Relation: "member", Object: "team:team-1"}, + ) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testUserUpdate} + + finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should update zanzana entry when team changes", func(t *testing.T) { + wg.Add(1) + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-3", + Namespace: "org-3", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionAdmin, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-3", + Namespace: "org-3", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-2", + }, + Permission: iamv0.TeamBindingTeamPermissionAdmin, + }, + } + + testTeamUpdate := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-3", req.Namespace) + + // Should delete old team binding + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 1) + require.Equal( + t, + req.Deletes.TupleKeys[0], + &v1.TupleKeyWithoutCondition{User: "user:user-1", Relation: "admin", Object: "team:team-1"}, + ) + + // Should write new team binding + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 1) + require.Equal( + t, + req.Writes.TupleKeys[0], + &v1.TupleKey{User: "user:user-1", Relation: "admin", Object: "team:team-2"}, + ) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testTeamUpdate} + + finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should not write to zanzana when update fails", func(t *testing.T) { + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-4", + Namespace: "org-4", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-4", + Namespace: "org-4", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-2", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + testNoWriteOnFailure := func(ctx context.Context, req *v1.WriteRequest) error { + // Should not be called when success=false + require.Fail(t, "Write should not be called when update fails") + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnFailure} + + finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call finish function with success=false + finishFunc(context.Background(), false) + // No wait needed since write should not be called + }) + + t.Run("should not write to zanzana when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-5", + Namespace: "org-5", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-5", + Namespace: "org-5", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-2", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + finishFunc, err := builder.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when zClient is nil + }) + + t.Run("should handle empty old binding subject name gracefully", func(t *testing.T) { + wg.Add(1) + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-6", + Namespace: "org-6", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "", // Empty name - conversion will be skipped + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-6", + Namespace: "org-6", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-2", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + testEmptyOldBinding := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-6", req.Namespace) + + // Should not delete old binding (it was skipped due to empty name) + require.Nil(t, req.Deletes) + + // Should write new binding + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 1) + require.Equal( + t, + req.Writes.TupleKeys[0], + &v1.TupleKey{User: "user:user-2", Relation: "member", Object: "team:team-1"}, + ) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testEmptyOldBinding} + + finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) // Should still return finish function + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should return nil finish func when bindings are identical", func(t *testing.T) { + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-7", + Namespace: "org-7", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-7", + Namespace: "org-7", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + writeCalled := false + testNoWriteOnNoChange := func(ctx context.Context, req *v1.WriteRequest) error { + writeCalled = true + require.Fail(t, "Write should not be called when bindings are identical") + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnNoChange} + + finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when bindings are identical + + // Verify write was never called + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when bindings are identical") + }) + + t.Run("should return nil finish func when new binding has empty subject name", func(t *testing.T) { + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-8", + Namespace: "org-8", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-8", + Namespace: "org-8", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "", // Empty name - should cause early return + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + writeCalled := false + testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.WriteRequest) error { + writeCalled = true + require.Fail(t, "Write should not be called when new binding has empty subject name") + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnInvalidBinding} + + finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when new binding has empty subject name + + // Verify write was never called + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when new binding has empty subject name") + }) + + t.Run("should return nil finish func when new binding has empty team ref name", func(t *testing.T) { + oldBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-9", + Namespace: "org-9", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + newBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-9", + Namespace: "org-9", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-2", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "", // Empty name - should cause early return + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + writeCalled := false + testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.WriteRequest) error { + writeCalled = true + require.Fail(t, "Write should not be called when new binding has empty team ref name") + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnInvalidBinding} + + finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when new binding has empty team ref name + + // Verify write was never called + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when new binding has empty team ref name") + }) +} + +func TestAfterTeamBindingDelete(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should delete zanzana entry for team binding with member permission", func(t *testing.T) { + wg.Add(1) + teamBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + External: false, + }, + } + + testMemberDelete := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + + // Should have deletes but no writes + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 1) + require.Nil(t, req.Writes) + + require.Equal( + t, + req.Deletes.TupleKeys[0], + &v1.TupleKeyWithoutCondition{User: "user:user-1", Relation: "member", Object: "team:team-1"}, + ) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testMemberDelete} + b.AfterTeamBindingDelete(&teamBinding, nil) + wg.Wait() + }) + + t.Run("should delete zanzana entry for team binding with admin permission", func(t *testing.T) { + wg.Add(1) + teamBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-2", + Namespace: "org-2", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-2", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-2", + }, + Permission: iamv0.TeamBindingTeamPermissionAdmin, + External: true, + }, + } + + testAdminDelete := func(ctx context.Context, req *v1.WriteRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + + // Should have deletes but no writes + require.NotNil(t, req.Deletes) + require.Len(t, req.Deletes.TupleKeys, 1) + require.Nil(t, req.Writes) + + require.Equal( + t, + req.Deletes.TupleKeys[0], + &v1.TupleKeyWithoutCondition{User: "user:user-2", Relation: "admin", Object: "team:team-2"}, + ) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testAdminDelete} + b.AfterTeamBindingDelete(&teamBinding, nil) + wg.Wait() + }) + + t.Run("should not delete from zanzana when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + teamBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-3", + Namespace: "org-3", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-3", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-3", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + // Should not panic or error when zClient is nil + builder.AfterTeamBindingDelete(&teamBinding, nil) + }) + + t.Run("should handle conversion error gracefully", func(t *testing.T) { + // TeamBinding with empty team ref name should fail conversion + teamBinding := iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-4", + Namespace: "org-4", + }, + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-4", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "", // Empty name should cause error + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + writeCalled := false + testErrorHandling := func(ctx context.Context, req *v1.WriteRequest) error { + writeCalled = true + // Should not be called due to conversion error + require.Fail(t, "Write should not be called when conversion fails") + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testErrorHandling} + b.AfterTeamBindingDelete(&teamBinding, nil) + // Wait a bit to ensure the goroutine has time to process + // The goroutine will complete but won't call the write callback + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when conversion fails") + }) +} + +func TestConvertTeamBindingToTuple(t *testing.T) { + t.Run("should convert member permission correctly", func(t *testing.T) { + tb := &iamv0.TeamBinding{ + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + tuple, err := convertTeamBindingToTuple(tb) + require.NoError(t, err) + require.NotNil(t, tuple) + require.Equal(t, "user:user-1", tuple.User) + require.Equal(t, "member", tuple.Relation) + require.Equal(t, "team:team-1", tuple.Object) + require.Nil(t, tuple.Condition) + }) + + t.Run("should convert admin permission correctly", func(t *testing.T) { + tb := &iamv0.TeamBinding{ + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-2", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-2", + }, + Permission: iamv0.TeamBindingTeamPermissionAdmin, + }, + } + + tuple, err := convertTeamBindingToTuple(tb) + require.NoError(t, err) + require.NotNil(t, tuple) + require.Equal(t, "user:user-2", tuple.User) + require.Equal(t, "admin", tuple.Relation) + require.Equal(t, "team:team-2", tuple.Object) + require.Nil(t, tuple.Condition) + }) + + t.Run("should return error for empty subject name", func(t *testing.T) { + tb := &iamv0.TeamBinding{ + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + tuple, err := convertTeamBindingToTuple(tb) + require.Error(t, err) + require.Nil(t, tuple) + require.Equal(t, errEmptyName, err) + }) + + t.Run("should return error for empty team ref name", func(t *testing.T) { + tb := &iamv0.TeamBinding{ + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "", + }, + Permission: iamv0.TeamBindingTeamPermissionMember, + }, + } + + tuple, err := convertTeamBindingToTuple(tb) + require.Error(t, err) + require.Nil(t, tuple) + require.Equal(t, errEmptyName, err) + }) + + t.Run("should default to member for unknown permission", func(t *testing.T) { + tb := &iamv0.TeamBinding{ + Spec: iamv0.TeamBindingSpec{ + Subject: iamv0.TeamBindingspecSubject{ + Name: "user-1", + }, + TeamRef: iamv0.TeamBindingTeamRef{ + Name: "team-1", + }, + Permission: "unknown", // Invalid permission + }, + } + + tuple, err := convertTeamBindingToTuple(tb) + require.NoError(t, err) + require.NotNil(t, tuple) + // Should default to member relation + require.Equal(t, "member", tuple.Relation) + }) +} diff --git a/pkg/registry/apis/iam/user_org_hooks.go b/pkg/registry/apis/iam/user_org_hooks.go new file mode 100644 index 00000000000..8f75ed5c970 --- /dev/null +++ b/pkg/registry/apis/iam/user_org_hooks.go @@ -0,0 +1,238 @@ +package iam + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic/registry" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +// AfterUserCreate is a post-create hook that writes the user's basic role assignment to Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterUserCreate(obj runtime.Object, _ *metav1.CreateOptions) { + if b.zClient == nil { + return + } + + user, ok := obj.(*iamv0.User) + if !ok { + b.logger.Error("failed to convert object to User type", "object", obj) + return + } + + // Skip if user has no role assigned + if user.Spec.Role == "" { + b.logger.Debug("user has no role assigned, skipping basic role sync", + "namespace", user.Namespace, + "name", user.Name, + ) + return + } + + resourceType := "user" + operation := "create" + + // Grab a ticket to write to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) + + go func(namespace, subjectName, role, resourceType, operation string) { + start := time.Now() + status := "success" + + defer func() { + <-b.zTickets + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc() + }() + + b.logger.Debug("writing user basic role to zanzana", + "namespace", namespace, + "name", subjectName, + "role", role, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: namespace, + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_UpdateUserOrgRole{ + UpdateUserOrgRole: &v1.UpdateUserOrgRoleOperation{User: subjectName, Role: role}, + }, + }, + }, + }) + + if err != nil { + status = "failure" + b.logger.Error("failed to write user basic role to zanzana", + "err", err, + "namespace", namespace, + "name", subjectName, + "role", role, + ) + } else { + hooksTuplesCounter.WithLabelValues(resourceType, operation, "write").Inc() + } + }(user.Namespace, user.Name, user.Spec.Role, resourceType, operation) +} + +// BeginUserUpdate is a pre-update hook that gets called on user updates +// It compares old and new roles and performs the zanzana write after K8s update succeeds +func (b *IdentityAccessManagementAPIBuilder) BeginUserUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) { + if b.zClient == nil { + return nil, nil + } + + oldUser, ok := oldObj.(*iamv0.User) + if !ok { + return nil, nil + } + + newUser, ok := obj.(*iamv0.User) + if !ok { + return nil, nil + } + + // If role hasn't changed, no need to update + if oldUser.Spec.Role == newUser.Spec.Role { + return nil, nil + } + + // Return a finish function that performs the zanzana write only on success + return func(ctx context.Context, success bool) { + if !success { + return + } + + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues("user", "update").Observe(time.Since(wait).Seconds()) + + go func(namespace, subjectName, oldRole, newRole string) { + start := time.Now() + status := "success" + + defer func() { + <-b.zTickets + hooksDurationHistogram.WithLabelValues("user", "update", status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues("user", "update", status).Inc() + }() + + b.logger.Debug("updating user basic role in zanzana", + "namespace", namespace, + "name", subjectName, + "oldRole", oldRole, + "newRole", newRole, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: namespace, + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_UpdateUserOrgRole{ + UpdateUserOrgRole: &v1.UpdateUserOrgRoleOperation{User: subjectName, Role: newRole}, + }, + }, { + Operation: &v1.MutateOperation_DeleteUserOrgRole{ + DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{User: subjectName, Role: oldRole}, + }, + }, + }, + }) + if err != nil { + status = "failure" + b.logger.Error("failed to update user basic role in zanzana", + "err", err, + "namespace", namespace, + "name", subjectName, + "role", newRole, + "oldRole", oldRole, + ) + } + }(oldUser.Namespace, oldUser.Name, oldUser.Spec.Role, newUser.Spec.Role) + }, nil +} + +// AfterUserDelete is a post-delete hook that removes the user's basic role assignment from Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterUserDelete(obj runtime.Object, _ *metav1.DeleteOptions) { + if b.zClient == nil { + return + } + + user, ok := obj.(*iamv0.User) + if !ok { + b.logger.Error("failed to convert object to User type", "object", obj) + return + } + + resourceType := "user" + operation := "delete" + + // Skip if user had no role assigned + if user.Spec.Role == "" { + b.logger.Debug("user had no role assigned, skipping basic role sync", + "namespace", user.Namespace, + "name", user.Name, + ) + return + } + + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) + + go func(namespace, subjectName, role string) { + start := time.Now() + status := "success" + + defer func() { + <-b.zTickets + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc() + }() + + b.logger.Debug("deleting user basic role from zanzana", + "namespace", namespace, + "name", subjectName, + "role", role, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: namespace, + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeleteUserOrgRole{ + DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{User: subjectName, Role: role}, + }, + }, + }, + }) + + if err != nil { + status = "failure" + b.logger.Error("failed to delete user basic role from zanzana", + "err", err, + "namespace", namespace, + "name", subjectName, + "role", role, + ) + } else { + hooksTuplesCounter.WithLabelValues(resourceType, operation, "delete").Inc() + } + }(user.Namespace, user.Name, user.Spec.Role) +} diff --git a/pkg/registry/apis/iam/user_org_hooks_test.go b/pkg/registry/apis/iam/user_org_hooks_test.go new file mode 100644 index 00000000000..021b067c33c --- /dev/null +++ b/pkg/registry/apis/iam/user_org_hooks_test.go @@ -0,0 +1,569 @@ +package iam + +import ( + "context" + "sync" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/stretchr/testify/require" +) + +func TestAfterUserCreate(t *testing.T) { + var wg sync.WaitGroup + + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should create zanzana entry for user with Admin role", func(t *testing.T) { + wg.Add(1) + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "df2p421det1q8c", + Namespace: "org-1", + }, + Spec: iamv0.UserSpec{ + Role: "Admin", + }, + } + + testAdminRole := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + require.Len(t, req.Operations, 1) + + op := req.Operations[0] + require.NotNil(t, op) + updateOp := op.GetUpdateUserOrgRole() + require.NotNil(t, updateOp) + require.Equal(t, "df2p421det1q8c", updateOp.User) + require.Equal(t, "Admin", updateOp.Role) + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testAdminRole} + b.AfterUserCreate(&user, nil) + wg.Wait() + }) + + t.Run("should create zanzana entry for user with Editor role", func(t *testing.T) { + wg.Add(1) + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user123", + Namespace: "org-2", + }, + Spec: iamv0.UserSpec{ + Role: "Editor", + }, + } + + testEditorRole := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + require.Len(t, req.Operations, 1) + + op := req.Operations[0] + require.NotNil(t, op) + updateOp := op.GetUpdateUserOrgRole() + require.NotNil(t, updateOp) + require.Equal(t, "user123", updateOp.User) + require.Equal(t, "Editor", updateOp.Role) + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testEditorRole} + b.AfterUserCreate(&user, nil) + wg.Wait() + }) + + t.Run("should create zanzana entry for user with Viewer role", func(t *testing.T) { + wg.Add(1) + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "viewer456", + Namespace: "org-3", + }, + Spec: iamv0.UserSpec{ + Role: "Viewer", + }, + } + + testViewerRole := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-3", req.Namespace) + require.Len(t, req.Operations, 1) + + op := req.Operations[0] + require.NotNil(t, op) + updateOp := op.GetUpdateUserOrgRole() + require.NotNil(t, updateOp) + require.Equal(t, "viewer456", updateOp.User) + require.Equal(t, "Viewer", updateOp.Role) + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testViewerRole} + b.AfterUserCreate(&user, nil) + wg.Wait() + }) + + t.Run("should skip when user has no role", func(t *testing.T) { + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "norole789", + Namespace: "org-4", + }, + Spec: iamv0.UserSpec{ + Role: "", + }, + } + + // Should not call zanzana client + b.zClient = nil + b.AfterUserCreate(&user, nil) + // If we get here without panic, the test passes + }) + + t.Run("should skip when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser", + Namespace: "org-1", + }, + Spec: iamv0.UserSpec{ + Role: "Admin", + }, + } + + // Should return early without calling zanzana + builder.AfterUserCreate(&user, nil) + // If we get here without panic, the test passes + }) +} + +func TestBeginUserUpdate(t *testing.T) { + var wg sync.WaitGroup + + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should update zanzana entry when role changes from Viewer to Admin", func(t *testing.T) { + wg.Add(1) + oldUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser", + Namespace: "org-1", + }, + Spec: iamv0.UserSpec{ + Role: "Viewer", + }, + } + + newUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser", + Namespace: "org-1", + }, + Spec: iamv0.UserSpec{ + Role: "Admin", + }, + } + + testRoleChange := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + require.Len(t, req.Operations, 2) + + // First operation should be UpdateUserOrgRole with new role + updateOp := req.Operations[0].GetUpdateUserOrgRole() + require.NotNil(t, updateOp) + require.Equal(t, "testuser", updateOp.User) + require.Equal(t, "Admin", updateOp.Role) + + // Second operation should be DeleteUserOrgRole with old role + deleteOp := req.Operations[1].GetDeleteUserOrgRole() + require.NotNil(t, deleteOp) + require.Equal(t, "testuser", deleteOp.User) + require.Equal(t, "Viewer", deleteOp.Role) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testRoleChange} + + finishFunc, err := b.BeginUserUpdate(context.Background(), &newUser, &oldUser, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should update role when new role is empty", func(t *testing.T) { + wg.Add(1) + oldUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser2", + Namespace: "org-2", + }, + Spec: iamv0.UserSpec{ + Role: "Editor", + }, + } + + newUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser2", + Namespace: "org-2", + }, + Spec: iamv0.UserSpec{ + Role: "", + }, + } + + testRemoveRole := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + require.Len(t, req.Operations, 2) + + // First operation should be UpdateUserOrgRole with empty role + updateOp := req.Operations[0].GetUpdateUserOrgRole() + require.NotNil(t, updateOp) + require.Equal(t, "testuser2", updateOp.User) + require.Equal(t, "", updateOp.Role) + + // Second operation should be DeleteUserOrgRole with old role + deleteOp := req.Operations[1].GetDeleteUserOrgRole() + require.NotNil(t, deleteOp) + require.Equal(t, "testuser2", deleteOp.User) + require.Equal(t, "Editor", deleteOp.Role) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testRemoveRole} + + finishFunc, err := b.BeginUserUpdate(context.Background(), &newUser, &oldUser, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should be able to add a new role when old role was empty", func(t *testing.T) { + wg.Add(1) + oldUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser3", + Namespace: "org-3", + }, + Spec: iamv0.UserSpec{ + Role: "", + }, + } + + newUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser3", + Namespace: "org-3", + }, + Spec: iamv0.UserSpec{ + Role: "Admin", + }, + } + + testAddRole := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-3", req.Namespace) + require.Len(t, req.Operations, 2) + + // First operation should be UpdateUserOrgRole with new role + updateOp := req.Operations[0].GetUpdateUserOrgRole() + require.NotNil(t, updateOp) + require.Equal(t, "testuser3", updateOp.User) + require.Equal(t, "Admin", updateOp.Role) + + // Second operation should be DeleteUserOrgRole with empty old role + deleteOp := req.Operations[1].GetDeleteUserOrgRole() + require.NotNil(t, deleteOp) + require.Equal(t, "testuser3", deleteOp.User) + require.Equal(t, "", deleteOp.Role) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testAddRole} + + finishFunc, err := b.BeginUserUpdate(context.Background(), &newUser, &oldUser, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should skip update when role hasn't changed", func(t *testing.T) { + oldUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser4", + Namespace: "org-4", + }, + Spec: iamv0.UserSpec{ + Role: "Editor", + }, + } + + newUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser4", + Namespace: "org-4", + }, + Spec: iamv0.UserSpec{ + Role: "Editor", + }, + } + + finishFunc, err := b.BeginUserUpdate(context.Background(), &newUser, &oldUser, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when no update needed + }) + + t.Run("should not call zanzana when update fails", func(t *testing.T) { + oldUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser5", + Namespace: "org-5", + }, + Spec: iamv0.UserSpec{ + Role: "Viewer", + }, + } + + newUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser5", + Namespace: "org-5", + }, + Spec: iamv0.UserSpec{ + Role: "Admin", + }, + } + + callCount := 0 + testNoCall := func(ctx context.Context, req *v1.MutateRequest) error { + callCount++ + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testNoCall} + + finishFunc, err := b.BeginUserUpdate(context.Background(), &newUser, &oldUser, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + // Call with success=false - should not trigger zanzana write + finishFunc(context.Background(), false) + require.Equal(t, 0, callCount, "zanzana should not be called when update fails") + }) + + t.Run("should skip when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + oldUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser", + Namespace: "org-1", + }, + Spec: iamv0.UserSpec{ + Role: "Viewer", + }, + } + + newUser := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser", + Namespace: "org-1", + }, + Spec: iamv0.UserSpec{ + Role: "Admin", + }, + } + + finishFunc, err := builder.BeginUserUpdate(context.Background(), &newUser, &oldUser, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when zClient is nil + }) +} + +func TestAfterUserDelete(t *testing.T) { + var wg sync.WaitGroup + + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should delete zanzana entry for user with Admin role", func(t *testing.T) { + wg.Add(1) + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "df2p421det1q8c", + Namespace: "org-1", + }, + Spec: iamv0.UserSpec{ + Role: "Admin", + }, + } + + testDeleteAdmin := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + require.Len(t, req.Operations, 1) + + op := req.Operations[0] + require.NotNil(t, op) + deleteOp := op.GetDeleteUserOrgRole() + require.NotNil(t, deleteOp) + require.Equal(t, "df2p421det1q8c", deleteOp.User) + require.Equal(t, "Admin", deleteOp.Role) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testDeleteAdmin} + b.AfterUserDelete(&user, nil) + wg.Wait() + }) + + t.Run("should delete zanzana entry for user with Editor role", func(t *testing.T) { + wg.Add(1) + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "editor123", + Namespace: "org-2", + }, + Spec: iamv0.UserSpec{ + Role: "Editor", + }, + } + + testDeleteEditor := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-2", req.Namespace) + require.Len(t, req.Operations, 1) + + op := req.Operations[0] + require.NotNil(t, op) + deleteOp := op.GetDeleteUserOrgRole() + require.NotNil(t, deleteOp) + require.Equal(t, "editor123", deleteOp.User) + require.Equal(t, "Editor", deleteOp.Role) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testDeleteEditor} + b.AfterUserDelete(&user, nil) + wg.Wait() + }) + + t.Run("should delete zanzana entry for user with Viewer role", func(t *testing.T) { + wg.Add(1) + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "viewer456", + Namespace: "org-3", + }, + Spec: iamv0.UserSpec{ + Role: "Viewer", + }, + } + + testDeleteViewer := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-3", req.Namespace) + require.Len(t, req.Operations, 1) + + op := req.Operations[0] + require.NotNil(t, op) + deleteOp := op.GetDeleteUserOrgRole() + require.NotNil(t, deleteOp) + require.Equal(t, "viewer456", deleteOp.User) + require.Equal(t, "Viewer", deleteOp.Role) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testDeleteViewer} + b.AfterUserDelete(&user, nil) + wg.Wait() + }) + + t.Run("should skip when user has no role", func(t *testing.T) { + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "norole789", + Namespace: "org-4", + }, + Spec: iamv0.UserSpec{ + Role: "", + }, + } + + // Should not call zanzana client + b.zClient = nil + b.AfterUserDelete(&user, nil) + // If we get here without panic, the test passes + }) + + t.Run("should skip when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + user := iamv0.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testuser", + Namespace: "org-1", + }, + Spec: iamv0.UserSpec{ + Role: "Admin", + }, + } + + // Should return early without calling zanzana + builder.AfterUserDelete(&user, nil) + // If we get here without panic, the test passes + }) +} diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index 59da5877250..cd6f8f0eb29 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -248,16 +248,6 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) { r = r.WithContext(ctx) - valid := b.validateNamespace(r) - b.logger.Debug("validating namespace in oneFlagHandler handler", "valid", valid) - if !valid { - _ = tracing.Errorf(span, namespaceMismatchMsg) - span.SetAttributes(semconv.HTTPStatusCode(http.StatusUnauthorized)) - b.logger.Error(namespaceMismatchMsg) - http.Error(w, namespaceMismatchMsg, http.StatusUnauthorized) - return - } - flagKey := mux.Vars(r)["flagKey"] if flagKey == "" { _ = tracing.Errorf(span, "flagKey parameter is required") @@ -266,6 +256,16 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) { return } + valid, ns := b.validateNamespace(r) + b.logger.Debug("validating namespace in oneFlagHandler handler", "namespace", ns, "valid", valid, "flag", flagKey) + if !valid { + _ = tracing.Errorf(span, namespaceMismatchMsg) + span.SetAttributes(semconv.HTTPStatusCode(http.StatusUnauthorized)) + b.logger.Error(namespaceMismatchMsg) + http.Error(w, namespaceMismatchMsg, http.StatusUnauthorized) + return + } + span.SetAttributes(attribute.String("flag_key", flagKey)) isAuthedReq := b.isAuthenticatedRequest(r) @@ -294,8 +294,8 @@ func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) { r = r.WithContext(ctx) - valid := b.validateNamespace(r) - b.logger.Debug("validating namespace in allFlagsHandler handler", "valid", valid) + valid, ns := b.validateNamespace(r) + b.logger.Debug("validating namespace in allFlagsHandler handler", "namespace", ns, "valid", valid) if !valid { _ = tracing.Errorf(span, namespaceMismatchMsg) @@ -359,14 +359,14 @@ func (b *APIBuilder) isAuthenticatedRequest(r *http.Request) bool { } // validateNamespace checks if the namespace in the evaluation context matches the namespace in the request -func (b *APIBuilder) validateNamespace(r *http.Request) bool { +func (b *APIBuilder) validateNamespace(r *http.Request) (bool, string) { _, span := tracing.Start(r.Context(), "ofrep.validateNamespace") defer span.End() var namespace string user, ok := types.AuthInfoFrom(r.Context()) if !ok { - return false + return false, "" } if user.GetNamespace() != "" { @@ -381,7 +381,7 @@ func (b *APIBuilder) validateNamespace(r *http.Request) bool { _ = tracing.Errorf(span, "failed to read request body: %w", err) b.logger.Error("Error reading evaluation request body", "error", err) span.SetAttributes(attribute.Bool("validation.success", false)) - return false + return false, "" } r.Body = io.NopCloser(bytes.NewBuffer(body)) @@ -391,9 +391,9 @@ func (b *APIBuilder) validateNamespace(r *http.Request) bool { // "default" namespace case can only occur in on-prem grafana if (namespace == "default" && evalCtxNamespace == "") || (evalCtxNamespace == namespace) { span.SetAttributes(attribute.Bool("validation.success", true)) - return true + return true, evalCtxNamespace } span.SetAttributes(attribute.Bool("validation.success", false)) - return false + return false, evalCtxNamespace } diff --git a/pkg/registry/apis/provisioning/accesscontrol.go b/pkg/registry/apis/provisioning/accesscontrol.go new file mode 100644 index 00000000000..e56ab7b06e2 --- /dev/null +++ b/pkg/registry/apis/provisioning/accesscontrol.go @@ -0,0 +1,129 @@ +package provisioning + +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org" +) + +const ( + // Repositories + ActionProvisioningRepositoriesCreate = "provisioning.repositories:create" // CREATE. + ActionProvisioningRepositoriesWrite = "provisioning.repositories:write" // UPDATE. + ActionProvisioningRepositoriesRead = "provisioning.repositories:read" // GET + LIST. + ActionProvisioningRepositoriesDelete = "provisioning.repositories:delete" // DELETE. + + // Jobs + ActionProvisioningJobsCreate = "provisioning.jobs:create" // CREATE. + ActionProvisioningJobsWrite = "provisioning.jobs:write" // UPDATE. + ActionProvisioningJobsRead = "provisioning.jobs:read" // GET + LIST. + ActionProvisioningJobsDelete = "provisioning.jobs:delete" // DELETE. + + // Historic Jobs + ActionProvisioningHistoricJobsRead = "provisioning.historicjobs:read" // GET + LIST. +) + +func registerAccessControlRoles(service accesscontrol.Service) error { + // Repositories + repositoriesReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.repositories:reader", + DisplayName: "Repositories Reader", + Description: "Read and list provisioning repositories.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningRepositoriesRead, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + repositoriesWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.repositories:writer", + DisplayName: "Repositories Writer", + Description: "Create, update and delete provisioning repositories.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningRepositoriesCreate, + }, + { + Action: ActionProvisioningRepositoriesRead, + }, + { + Action: ActionProvisioningRepositoriesWrite, + }, + { + Action: ActionProvisioningRepositoriesDelete, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + // Jobs + jobsReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.jobs:reader", + DisplayName: "Jobs Reader", + Description: "Read and list provisioning jobs.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningJobsRead, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + jobsWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.jobs:writer", + DisplayName: "Jobs Writer", + Description: "Create, update and delete provisioning jobs.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningJobsCreate, + }, + { + Action: ActionProvisioningJobsRead, + }, + { + Action: ActionProvisioningJobsWrite, + }, + { + Action: ActionProvisioningJobsDelete, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + // Historic Jobs + historicJobsReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:provisioning.historicjobs:reader", + DisplayName: "Historic Jobs Reader", + Description: "Read and list provisioning historic jobs.", + Group: "Provisioning", + Permissions: []accesscontrol.Permission{ + { + Action: ActionProvisioningHistoricJobsRead, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + return service.DeclareFixedRoles( + repositoriesReader, + repositoriesWriter, + jobsReader, + jobsWriter, + historicJobsReader, + ) +} diff --git a/pkg/registry/apis/provisioning/controller/finalizers.go b/pkg/registry/apis/provisioning/controller/finalizers.go index d9c41ee4c15..5bd468fe660 100644 --- a/pkg/registry/apis/provisioning/controller/finalizers.go +++ b/pkg/registry/apis/provisioning/controller/finalizers.go @@ -7,8 +7,11 @@ import ( "slices" "sort" "strings" + "sync/atomic" "time" + "github.com/grafana/dskit/concurrency" + "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -27,6 +30,7 @@ type finalizer struct { lister resources.ResourceLister clientFactory resources.ClientFactory metrics *finalizerMetrics + maxWorkers int } func (f *finalizer) process(ctx context.Context, @@ -113,27 +117,73 @@ func (f *finalizer) processExistingItems( // Safe deletion order sortResourceListForDeletion(items) - count := 0 + var dashboards, folderItems []*provisioning.ResourceListItem for _, item := range items.Items { - res, _, err := clients.ForResource(ctx, schema.GroupVersionResource{ + if item.Group == folders.GroupVersion.Group { + folderItems = append(folderItems, &item) + } else { + dashboards = append(dashboards, &item) + } + } + + processItem := func(jobCtx context.Context, item *provisioning.ResourceListItem) error { + res, _, err := clients.ForResource(jobCtx, schema.GroupVersionResource{ Group: item.Group, Resource: item.Resource, }) if err != nil { logger.Error("error getting client for resource", "resource", item.Resource, "error", err) - return count, err + return err } - err = cb(res, &item) + err = cb(res, item) if err != nil { + if errors.IsNotFound(err) { + logger.Info("resource not found, skipping", "name", item.Name, "group", item.Group, "resource", item.Resource) + return nil + } logger.Error("error processing item", "name", item.Name, "error", err) - return count, fmt.Errorf("processing item: %w", err) - } else { + return fmt.Errorf("processing item: %w", err) + } + return nil + } + + processGroup := func(group []*provisioning.ResourceListItem) (int, error) { + var processed int64 + err := concurrency.ForEachJob(ctx, len(group), f.maxWorkers, func(ctx context.Context, idx int) error { + jobCtx, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + item := group[idx] + if err := processItem(jobCtx, item); err != nil { + return err + } + atomic.AddInt64(&processed, 1) + return nil + }) + return int(processed), err + } + + count := 0 + + if len(dashboards) > 0 { + processed, err := processGroup(dashboards) + if err != nil { + return processed, err + } + count += processed + } + + if len(folderItems) > 0 { + for _, item := range folderItems { + if err := processItem(ctx, item); err != nil { + return count, err + } count++ } } - logger.Info("processed orphan items", "items", count) + + logger.Info("processed items", "items", count) return count, nil } diff --git a/pkg/registry/apis/provisioning/controller/finalizers_test.go b/pkg/registry/apis/provisioning/controller/finalizers_test.go index 140363a0507..4df89107bbe 100644 --- a/pkg/registry/apis/provisioning/controller/finalizers_test.go +++ b/pkg/registry/apis/provisioning/controller/finalizers_test.go @@ -2,10 +2,15 @@ package controller import ( "context" + "fmt" + "sync" + "sync/atomic" "testing" + "time" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -14,6 +19,8 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" + "github.com/grafana/grafana-app-sdk/logging" + folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" @@ -140,7 +147,7 @@ func TestFinalizer_process(t *testing.T) { resourceLister := resources.NewMockResourceLister(t) resourceLister. - On("List", context.Background(), "default", "my-repo"). + On("List", mock.Anything, "default", "my-repo"). Once(). Return(&provisioning.ResourceList{ Items: []provisioning.ResourceListItem{ @@ -164,12 +171,12 @@ func TestFinalizer_process(t *testing.T) { } clientFactory. - On("Clients", context.Background(), "default"). + On("Clients", mock.Anything, "default"). Once(). Return(clients, nil) clients. - On("ForResource", context.Background(), schema.GroupVersionResource{ + On("ForResource", mock.Anything, schema.GroupVersionResource{ Group: "dashboard.grafana.app", Resource: "dashboards", }). @@ -196,7 +203,7 @@ func TestFinalizer_process(t *testing.T) { resourceLister := resources.NewMockResourceLister(t) resourceLister. - On("List", context.Background(), "default", "my-repo"). + On("List", mock.Anything, "default", "my-repo"). Once(). Return(&provisioning.ResourceList{ Items: []provisioning.ResourceListItem{ @@ -220,12 +227,12 @@ func TestFinalizer_process(t *testing.T) { } clientFactory. - On("Clients", context.Background(), "default"). + On("Clients", mock.Anything, "default"). Once(). Return(clients, nil) clients. - On("ForResource", context.Background(), schema.GroupVersionResource{ + On("ForResource", mock.Anything, schema.GroupVersionResource{ Group: "dashboard.grafana.app", Resource: "dashboards", }). @@ -253,7 +260,7 @@ func TestFinalizer_process(t *testing.T) { clientFactory := resources.NewMockClientFactory(t) clientFactory. - On("Clients", context.Background(), "default"). + On("Clients", mock.Anything, "default"). Once(). Return(nil, assert.AnError) @@ -275,7 +282,7 @@ func TestFinalizer_process(t *testing.T) { resourceLister := resources.NewMockResourceLister(t) resourceLister. - On("List", context.Background(), "default", "my-repo"). + On("List", mock.Anything, "default", "my-repo"). Once(). Return(nil, assert.AnError) @@ -286,7 +293,7 @@ func TestFinalizer_process(t *testing.T) { clients := resources.NewMockResourceClients(t) clientFactory. - On("Clients", context.Background(), "default"). + On("Clients", mock.Anything, "default"). Once(). Return(clients, nil) @@ -308,7 +315,7 @@ func TestFinalizer_process(t *testing.T) { resourceLister := resources.NewMockResourceLister(t) resourceLister. - On("List", context.Background(), "default", "my-repo"). + On("List", mock.Anything, "default", "my-repo"). Once(). Return(&provisioning.ResourceList{ Items: []provisioning.ResourceListItem{ @@ -327,12 +334,12 @@ func TestFinalizer_process(t *testing.T) { clients := resources.NewMockResourceClients(t) clientFactory. - On("Clients", context.Background(), "default"). + On("Clients", mock.Anything, "default"). Once(). Return(clients, nil) clients. - On("ForResource", context.Background(), schema.GroupVersionResource{ + On("ForResource", mock.Anything, schema.GroupVersionResource{ Group: "dashboard.grafana.app", Resource: "dashboards", }). @@ -357,7 +364,7 @@ func TestFinalizer_process(t *testing.T) { resourceLister := resources.NewMockResourceLister(t) resourceLister. - On("List", context.Background(), "default", "my-repo"). + On("List", mock.Anything, "default", "my-repo"). Once(). Return(&provisioning.ResourceList{ Items: []provisioning.ResourceListItem{ @@ -381,12 +388,12 @@ func TestFinalizer_process(t *testing.T) { } clientFactory. - On("Clients", context.Background(), "default"). + On("Clients", mock.Anything, "default"). Once(). Return(clients, nil) clients. - On("ForResource", context.Background(), schema.GroupVersionResource{ + On("ForResource", mock.Anything, schema.GroupVersionResource{ Group: "dashboard.grafana.app", Resource: "dashboards", }). @@ -414,7 +421,7 @@ func TestFinalizer_process(t *testing.T) { resourceLister := resources.NewMockResourceLister(t) resourceLister. - On("List", context.Background(), "default", "my-repo"). + On("List", mock.Anything, "default", "my-repo"). Once(). Return(&provisioning.ResourceList{ Items: []provisioning.ResourceListItem{ @@ -438,12 +445,12 @@ func TestFinalizer_process(t *testing.T) { } clientFactory. - On("Clients", context.Background(), "default"). + On("Clients", mock.Anything, "default"). Once(). Return(clients, nil) clients. - On("ForResource", context.Background(), schema.GroupVersionResource{ + On("ForResource", mock.Anything, schema.GroupVersionResource{ Group: "dashboard.grafana.app", Resource: "dashboards", }). @@ -560,3 +567,142 @@ func TestSortResourceListForDeletion(t *testing.T) { }) } } + +func TestFinalizer_processExistingItems_Concurrency(t *testing.T) { + testCases := []struct { + name string + dashboardCount int + folderCount int + maxWorkers int + expectedConcurrency bool + }{ + { + name: "Multiple dashboards processed concurrently", + dashboardCount: 10, + folderCount: 0, + maxWorkers: 5, + expectedConcurrency: true, + }, + { + name: "Single worker processes dashboards sequentially", + dashboardCount: 5, + folderCount: 0, + maxWorkers: 1, + expectedConcurrency: false, + }, + { + name: "Folders processed sequentially regardless of maxWorkers", + dashboardCount: 0, + folderCount: 5, + maxWorkers: 10, + expectedConcurrency: false, + }, + { + name: "Mixed dashboards and folders - dashboards concurrent, folders sequential", + dashboardCount: 10, + folderCount: 3, + maxWorkers: 5, + expectedConcurrency: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Will be used to track concurrent executions + var ( + concurrentCount int64 + maxConcurrent int64 + mu sync.Mutex + ) + + items := provisioning.ResourceList{Items: []provisioning.ResourceListItem{}} + + for i := 0; i < tc.dashboardCount; i++ { + items.Items = append(items.Items, provisioning.ResourceListItem{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: fmt.Sprintf("dashboard-%d", i), + }) + } + + for i := 0; i < tc.folderCount; i++ { + items.Items = append(items.Items, provisioning.ResourceListItem{ + Group: folders.GroupVersion.Group, + Resource: "folders", + Name: fmt.Sprintf("folder-%d", i), + }) + } + + resourceLister := resources.NewMockResourceLister(t) + resourceLister. + On("List", mock.Anything, "default", "my-repo"). + Return(&items, nil) + + clientFactory := resources.NewMockClientFactory(t) + clients := resources.NewMockResourceClients(t) + + client := &mockDynamicClient{ + deleteFunc: func(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { + // Track concurrent executions + current := atomic.AddInt64(&concurrentCount, 1) + defer atomic.AddInt64(&concurrentCount, -1) + + mu.Lock() + if current > maxConcurrent { + maxConcurrent = current + } + mu.Unlock() + + // Simulate slow client to allow concurrency to build up + time.Sleep(1 * time.Second) + + return nil + }, + } + + clientFactory. + On("Clients", mock.Anything, "default"). + Return(clients, nil) + + clients. + On("ForResource", mock.Anything, mock.Anything). + Return(client, schema.GroupVersionKind{}, nil) + + metrics := registerFinalizerMetrics(prometheus.NewRegistry()) + f := &finalizer{ + lister: resourceLister, + clientFactory: clientFactory, + metrics: &metrics, + maxWorkers: tc.maxWorkers, + } + + repo := &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-repo", + Namespace: "default", + }, + } + + count, err := f.processExistingItems( + context.Background(), + repo, + f.removeResources(context.Background(), logging.DefaultLogger), + ) + + assert.NoError(t, err) + assert.Equal(t, tc.dashboardCount+tc.folderCount, count) + + if tc.expectedConcurrency { + // When concurrent, max concurrent should be > 1 + assert.Greater(t, maxConcurrent, int64(1), + "Expected concurrent execution but maxConcurrent was %d", maxConcurrent) + // Should not exceed maxWorkers + assert.LessOrEqual(t, maxConcurrent, int64(tc.maxWorkers)) + } else { + // When sequential, max concurrent should be 1 + assert.Equal(t, int64(1), maxConcurrent, + "Expected sequential execution but maxConcurrent was %d", maxConcurrent) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/controller/repository.go b/pkg/registry/apis/provisioning/controller/repository.go index 70e5c3b4a87..ffeb3f1fb9f 100644 --- a/pkg/registry/apis/provisioning/controller/repository.go +++ b/pkg/registry/apis/provisioning/controller/repository.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel/attribute" apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -84,6 +85,7 @@ func NewRepositoryController( statusPatcher StatusPatcher, registry prometheus.Registerer, tracer tracing.Tracer, + parallelOperations int, ) (*RepositoryController, error) { finalizerMetrics := registerFinalizerMetrics(registry) @@ -104,6 +106,7 @@ func NewRepositoryController( lister: resourceLister, clientFactory: clients, metrics: &finalizerMetrics, + maxWorkers: parallelOperations, }, jobs: jobs, logger: logging.DefaultLogger.With("logger", loggerName), @@ -138,6 +141,9 @@ func repoKeyFunc(obj any) (string, error) { } // Run starts the RepositoryController. +// +// Note: This function intentionally does NOT create a tracing span because it runs indefinitely +// until shutdown. Individual processing operations already have their own spans. func (rc *RepositoryController) Run(ctx context.Context, workerCount int) { defer utilruntime.HandleCrash() defer rc.queue.ShutDown() @@ -384,50 +390,75 @@ func shouldUseIncrementalSync(ctx context.Context, versioned repository.Versione } func (rc *RepositoryController) addSyncJob(ctx context.Context, obj *provisioning.Repository, syncOptions *provisioning.SyncJobOptions) error { + ctx, span := rc.tracer.Start(ctx, "provisioning.controller.add_sync_job") + defer span.End() + + span.SetAttributes( + attribute.String("repository", obj.GetName()), + attribute.String("namespace", obj.Namespace), + attribute.Bool("incremental", syncOptions != nil && syncOptions.Incremental), + ) + job, err := rc.jobs.Insert(ctx, obj.Namespace, provisioning.JobSpec{ Repository: obj.GetName(), Action: provisioning.JobActionPull, Pull: syncOptions, }) if apierrors.IsAlreadyExists(err) { - logging.FromContext(ctx).Info("sync job already exists, nothing triggered") + logging.FromContext(ctx).Info("sync job already exists") return nil } if err != nil { + span.RecordError(err) // FIXME: should we update the status of the repository if we fail to add the job? return fmt.Errorf("error adding sync job: %w", err) } - logging.FromContext(ctx).Info("sync job triggered", "job", job.Name) + span.SetAttributes(attribute.String("job.name", job.Name)) return nil } -func (rc *RepositoryController) determineSyncStatus(obj *provisioning.Repository, syncOptions *provisioning.SyncJobOptions, healthStatus provisioning.HealthStatus) *provisioning.SyncStatus { +func (rc *RepositoryController) determineSyncStatusOps(obj *provisioning.Repository, syncOptions *provisioning.SyncJobOptions, healthStatus provisioning.HealthStatus) []map[string]interface{} { const unhealthyMessage = "Repository is unhealthy" hasUnhealthyMessage := len(obj.Status.Sync.Message) > 0 && obj.Status.Sync.Message[0] == unhealthyMessage + var patchOperations []map[string]interface{} + switch { case syncOptions != nil: - return &provisioning.SyncStatus{ - State: provisioning.JobStatePending, - LastRef: obj.Status.Sync.LastRef, - Started: time.Now().UnixMilli(), - } + // We will try to trigger a new sync job if we have sync options + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/sync/state", + "value": provisioning.JobStatePending, + }) + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/sync/started", + "value": int64(0), + }) case healthStatus.Healthy && hasUnhealthyMessage: // if the repository is healthy and the message is set, clear it // FIXME: is this the clearest way to do this? Should we introduce another status or way of way of handling more // specific errors? - return &provisioning.SyncStatus{ - LastRef: obj.Status.Sync.LastRef, - } + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/sync/message", + "value": []string{}, + }) case !healthStatus.Healthy && !hasUnhealthyMessage: // if the repository is unhealthy and the message is not already set, set it - return &provisioning.SyncStatus{ - State: provisioning.JobStateError, - Message: []string{unhealthyMessage}, - LastRef: obj.Status.Sync.LastRef, - } - default: - return nil + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/sync/state", + "value": provisioning.JobStateError, + }) + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": "/status/sync/message", + "value": []string{unhealthyMessage}, + }) } + + return patchOperations } //nolint:gocyclo @@ -507,13 +538,7 @@ func (rc *RepositoryController) process(item *queueItem) error { // determine the sync strategy and sync status to apply syncOptions := rc.determineSyncStrategy(ctx, obj, repo, shouldResync, healthStatus) - if syncStatus := rc.determineSyncStatus(obj, syncOptions, healthStatus); syncStatus != nil { - patchOperations = append(patchOperations, map[string]interface{}{ - "op": "replace", - "path": "/status/sync", - "value": syncStatus, - }) - } + patchOperations = append(patchOperations, rc.determineSyncStatusOps(obj, syncOptions, healthStatus)...) // Apply all patch operations if len(patchOperations) > 0 { @@ -523,6 +548,8 @@ func (rc *RepositoryController) process(item *queueItem) error { } } + // QUESTION: should we trigger the sync job after we have applied all patch operations or before? + // Is there are risk of race condition here? // Trigger sync job after we have applied all patch operations if syncOptions != nil { if err := rc.addSyncJob(ctx, obj, syncOptions); err != nil { diff --git a/pkg/registry/apis/provisioning/dependencies.go b/pkg/registry/apis/provisioning/dependencies.go new file mode 100644 index 00000000000..f573fb59a80 --- /dev/null +++ b/pkg/registry/apis/provisioning/dependencies.go @@ -0,0 +1,24 @@ +package provisioning + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +// DependencyRegisterer is set to satisfy wire gen and make sure the `RegisterDependencies` is called. +type DependencyRegisterer struct{} + +func RegisterDependencies( + cfg *setting.Cfg, + accessControlService accesscontrol.Service, + features featuremgmt.FeatureToggles, +) (*DependencyRegisterer, error) { + if err := registerAccessControlRoles(accessControlService); err != nil { + return nil, fmt.Errorf("registering access control roles: %w", err) + } + + return &DependencyRegisterer{}, nil +} diff --git a/pkg/registry/apis/provisioning/jobs.go b/pkg/registry/apis/provisioning/jobs.go index b1e174a04b7..94b705414f8 100644 --- a/pkg/registry/apis/provisioning/jobs.go +++ b/pkg/registry/apis/provisioning/jobs.go @@ -132,28 +132,36 @@ func (c *jobsConnector) Connect( } spec.Repository = name - // If a sync job is being created, we should update its status to pending. + job, err := c.jobs.GetJobQueue().Insert(ctx, cfg.Namespace, spec) + if err != nil { + responder.Error(err) + return + } + + // For pull jobs update the sync status + // patch the sync status 'state' to 'pending', and reset the 'started' field, leaving other fields unchanged. + // Intentionally maintain the previous job name until the jobs is picked up. if spec.Pull != nil { - err = c.statusPatcherProvider.GetStatusPatcher().Patch(ctx, cfg, map[string]interface{}{ - "op": "replace", - "path": "/status/sync", - "value": &provisioning.SyncStatus{ - State: provisioning.JobStatePending, - LastRef: cfg.Status.Sync.LastRef, - Started: time.Now().UnixMilli(), + err = c.statusPatcherProvider.GetStatusPatcher().Patch(ctx, cfg, + map[string]interface{}{ + "op": "replace", + "path": "/status/sync/state", + "value": provisioning.JobStatePending, }, - }) + map[string]interface{}{ + // Use "replace" instead of "remove" since "remove" fails if the path does not exist (RFC 6902). + // "started" field uses "omitempty", so it may be missing in the JSON. + "op": "replace", + "path": "/status/sync/started", + "value": int64(0), + }, + ) if err != nil { responder.Error(err) return } } - job, err := c.jobs.GetJobQueue().Insert(ctx, cfg.Namespace, spec) - if err != nil { - responder.Error(err) - return - } responder.Object(http.StatusAccepted, job) }), 30*time.Second), nil } diff --git a/pkg/registry/apis/provisioning/jobs/concurrent_driver.go b/pkg/registry/apis/provisioning/jobs/concurrent_driver.go index 1fdc32bea60..dce8d65a97e 100644 --- a/pkg/registry/apis/provisioning/jobs/concurrent_driver.go +++ b/pkg/registry/apis/provisioning/jobs/concurrent_driver.go @@ -75,9 +75,12 @@ func NewConcurrentJobDriver( // Run starts multiple job drivers concurrently and handles cleanup coordination. // This is a blocking function that will run until the context is canceled or an error occurs. +// +// Note: This function intentionally does NOT create a tracing span because it runs indefinitely +// until shutdown. Individual job processing and cleanup operations already have their own spans. func (c *ConcurrentJobDriver) Run(ctx context.Context) error { logger := logging.FromContext(ctx).With("logger", "concurrent-job-driver", "num_drivers", c.numDrivers) - logger.Info("starting concurrent job driver with lease-based cleanup", "cleanup_interval", c.cleanupInterval) + logger.Info("start concurrent job driver", "num_drivers", c.numDrivers, "cleanup_interval", c.cleanupInterval) // Set up cleanup ticker - runs more frequently with lease-based approach cleanupTicker := time.NewTicker(c.cleanupInterval) @@ -85,7 +88,7 @@ func (c *ConcurrentJobDriver) Run(ctx context.Context) error { // Initial cleanup if err := c.store.Cleanup(ctx); err != nil { - logger.Error("failed to clean up old jobs at start", "error", err) + logger.Error("failed initial cleanup", "error", err) } var wg sync.WaitGroup @@ -99,10 +102,10 @@ func (c *ConcurrentJobDriver) Run(ctx context.Context) error { select { case <-cleanupTicker.C: if err := c.store.Cleanup(ctx); err != nil { - logger.Error("failed to cleanup jobs", "error", err) + logger.Error("failed cleanup", "error", err) } case <-ctx.Done(): - logger.Debug("cleanup goroutine stopping") + logger.Debug("cleanup routine stopped") return } } @@ -133,13 +136,13 @@ func (c *ConcurrentJobDriver) Run(ctx context.Context) error { return } - driverLogger.Debug("starting job driver") + driverLogger.Info("start job driver") if err := driver.Run(driverCtx); err != nil { driverLogger.Error("job driver failed", "error", err) errChan <- err return } - driverLogger.Debug("job driver stopped") + driverLogger.Info("job driver stopped") }(i) } @@ -157,6 +160,10 @@ func (c *ConcurrentJobDriver) Run(ctx context.Context) error { } } - logger.Info("all job driver workers stopped") - return ctx.Err() + if ctx.Err() != nil { + logger.Info("all job drivers gracefully stopped") + return nil + } + + return fmt.Errorf("concurrent job driver stopped unexpectedly") } diff --git a/pkg/registry/apis/provisioning/jobs/driver.go b/pkg/registry/apis/provisioning/jobs/driver.go index bad1df13759..b1cb0ee6da1 100644 --- a/pkg/registry/apis/provisioning/jobs/driver.go +++ b/pkg/registry/apis/provisioning/jobs/driver.go @@ -4,8 +4,10 @@ import ( "context" "errors" "strings" + "sync" "time" + "go.opentelemetry.io/otel/attribute" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apiserver/pkg/endpoints/request" @@ -13,6 +15,7 @@ import ( "github.com/grafana/grafana/apps/provisioning/pkg/apifmt" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" ) // Store is an abstraction for the storage API. @@ -74,6 +77,11 @@ type jobDriver struct { // notifications channel for job create events notifications chan struct{} + + // Mutex to protect concurrent access to job processing + mu sync.Mutex + // currentJob is the job currently being processed + currentJob *provisioning.Job } func NewJobDriver( @@ -99,6 +107,9 @@ func NewJobDriver( // Run drives jobs to completion. This is a blocking function. // It will run until the context is canceled or an error occurs. // This is a thread-safe function; it may be called from multiple goroutines. +// +// Note: This function intentionally does NOT create a tracing span because it runs indefinitely +// until shutdown. Individual job processing operations already have their own spans. func (d *jobDriver) Run(ctx context.Context) error { jobTicker := time.NewTicker(d.jobInterval) defer jobTicker.Stop() @@ -116,7 +127,8 @@ func (d *jobDriver) Run(ctx context.Context) error { for { select { case <-ctx.Done(): - return ctx.Err() + logger.Info("job driver stopped") + return nil // Context cancellation is expected during shutdown case <-jobTicker.C: d.processJobsUntilDoneOrError(ctx) case <-d.notifications: @@ -128,6 +140,11 @@ func (d *jobDriver) Run(ctx context.Context) error { // This will keep processing jobs until there are none left (or we hit an error) func (d *jobDriver) processJobsUntilDoneOrError(ctx context.Context) { for { + // Check if context is cancelled before attempting to claim jobs + if ctx.Err() != nil { + return + } + err := d.claimAndProcessOneJob(ctx) if err != nil { if !errors.Is(err, ErrNoJobs) { @@ -139,25 +156,39 @@ func (d *jobDriver) processJobsUntilDoneOrError(ctx context.Context) { } func (d *jobDriver) claimAndProcessOneJob(ctx context.Context) error { + ctx, span := tracing.Start(ctx, "provisioning.jobs.claim_and_process_one_job") + defer span.End() + logger := logging.FromContext(ctx) // Claim a job to work on. - job, rollback, err := d.store.Claim(ctx) + claimedJob, rollback, err := d.store.Claim(ctx) if err != nil { + if !errors.Is(err, ErrNoJobs) { + span.RecordError(err) + } return apifmt.Errorf("failed to claim job: %w", err) } // Ensure that the job is cleaned up if we fail to complete it. // The rollback function does not care about cancellations. defer rollback() - logger = logger.With("job", job.GetName(), "namespace", job.GetNamespace()) + namespace := claimedJob.GetNamespace() + logger = logger.With("job", claimedJob.GetName(), "namespace", namespace) ctx = logging.Context(ctx, logger) - logger.Debug("claimed a job") + d.currentJob = claimedJob + + span.SetAttributes( + attribute.String("job.name", claimedJob.GetName()), + attribute.String("job.namespace", namespace), + attribute.String("job.repository", claimedJob.Spec.Repository), + attribute.String("job.action", string(claimedJob.Spec.Action)), + ) // Now that we have a job, we need to augment our namespace to grant ourselves permission to work on it. // Incidentally, this also limits our permissions to only the namespace of the job. - ctx = request.WithNamespace(ctx, job.GetNamespace()) - ctx, _, err = identity.WithProvisioningIdentity(ctx, job.GetNamespace()) + ctx = request.WithNamespace(ctx, namespace) + ctx, _, err = identity.WithProvisioningIdentity(ctx, namespace) if err != nil { return apifmt.Errorf("failed to grant provisioning identity: %w", err) } @@ -169,50 +200,72 @@ func (d *jobDriver) claimAndProcessOneJob(ctx context.Context) error { leaseRenewalCtx, cancelLeaseRenewal := context.WithCancel(jobctx) leaseExpired := make(chan struct{}) - go d.leaseRenewalLoop(leaseRenewalCtx, job, logger, leaseExpired) + go d.leaseRenewalLoop(leaseRenewalCtx, logger, leaseExpired) defer cancelLeaseRenewal() - recorder := newJobProgressRecorder(d.onProgress(job)) + recorder := newJobProgressRecorder(d.onProgress()) + recorder.SetMessage(ctx, "start job") // Process the job with lease loss detection - start := time.Now() - job.Status.Started = start.UnixMilli() - err = d.processJobWithLeaseCheck(jobctx, job, recorder, leaseExpired) + err = d.processJobWithLeaseCheck(jobctx, recorder, leaseExpired) end := time.Now() - logger.Debug("job processed", "duration", end.Sub(start), "error", err) + logger.Debug("job processed", "duration", end.Sub(recorder.Started()), "error", err) - // Capture job timeout - if jobctx.Err() != nil && err == nil { + // Check if parent context was cancelled (graceful shutdown) + if ctx.Err() != nil { + logger.Debug("context cancel - job will retry") + // Don't complete the job - let it be retried by another worker + d.mu.Lock() + d.currentJob = nil + d.mu.Unlock() + return nil + } + + // Capture job timeout (but not parent context cancellation) + if jobctx.Err() != nil && err == nil && ctx.Err() == nil { err = jobctx.Err() } - job.Status = recorder.Complete(ctx, err) + // Record job processing error on span + if err != nil { + span.RecordError(err) + } + + // Complete the job + d.mu.Lock() + d.currentJob.Status = recorder.Complete(ctx, err) + defer func() { + d.currentJob = nil + d.mu.Unlock() + }() // Save the finished job - err = d.historicJobs.WriteJob(ctx, job.DeepCopy()) + err = d.historicJobs.WriteJob(ctx, d.currentJob.DeepCopy()) if err != nil { // We're not going to return this as it is not critical. Not ideal, but not critical. - logger.Warn("failed to create historic job", "historic_job", *job, "error", err) - } else { - logger.Debug("created historic job", "historic_job", *job) + logger.Warn("failed to write historic job", "error", err) } // Mark the job as completed. - if err := d.store.Complete(ctx, job); err != nil { - return apifmt.Errorf("failed to complete job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + if err := d.store.Complete(ctx, d.currentJob); err != nil { + span.RecordError(err) + return apifmt.Errorf("failed to complete job '%s' in '%s': %w", d.currentJob.GetName(), d.currentJob.GetNamespace(), err) } - logger.Debug("job completed") + logger.Info("job complete") return nil } // leaseRenewalLoop continuously renews the lease for a job until the context is cancelled. // If lease renewal fails persistently, it signals via the leaseExpired channel. -func (d *jobDriver) leaseRenewalLoop(ctx context.Context, job *provisioning.Job, logger logging.Logger, leaseExpired chan struct{}) { +// +// Note: This function intentionally does NOT create a tracing span because it runs indefinitely +// for the lifetime of a job. Individual RenewLease calls already have their own spans. +func (d *jobDriver) leaseRenewalLoop(ctx context.Context, logger logging.Logger, leaseExpired chan struct{}) { ticker := time.NewTicker(d.leaseRenewalInterval) defer ticker.Stop() - logger.Debug("starting lease renewal loop", "renewal_interval", d.leaseRenewalInterval) + logger.Debug("start lease renewal loop", "renewal_interval", d.leaseRenewalInterval) consecutiveFailures := 0 maxFailures := 3 // Allow a few failures before giving up @@ -220,10 +273,18 @@ func (d *jobDriver) leaseRenewalLoop(ctx context.Context, job *provisioning.Job, for { select { case <-ctx.Done(): - logger.Debug("lease renewal loop stopping") + logger.Debug("lease renewal loop stopped") return case <-ticker.C: - err := d.store.RenewLease(ctx, job) + d.mu.Lock() + if d.currentJob == nil { + d.mu.Unlock() + return + } + + err := d.store.RenewLease(ctx, d.currentJob) + d.mu.Unlock() + if err != nil { consecutiveFailures++ if apierrors.IsNotFound(err) || @@ -246,18 +307,17 @@ func (d *jobDriver) leaseRenewalLoop(ctx context.Context, job *provisioning.Job, logger.Debug("lease renewal recovered", "previous_failures", consecutiveFailures) } consecutiveFailures = 0 - logger.Debug("lease renewed successfully") } } } } -// processJobWithLeaseCheck processes a job but aborts if the lease expires. -func (d *jobDriver) processJobWithLeaseCheck(ctx context.Context, job *provisioning.Job, recorder JobProgressRecorder, leaseExpired <-chan struct{}) error { +// processJobWithLeaseCheck processes a job but aborts if the lease expires or context is cancelled. +func (d *jobDriver) processJobWithLeaseCheck(ctx context.Context, recorder JobProgressRecorder, leaseExpired <-chan struct{}) error { // Run the job processing in a goroutine so we can monitor lease expiry resultChan := make(chan error, 1) go func() { - resultChan <- d.processJob(ctx, job, recorder) + resultChan <- d.processJob(ctx, recorder) }() select { @@ -266,25 +326,48 @@ func (d *jobDriver) processJobWithLeaseCheck(ctx context.Context, job *provision case <-leaseExpired: return apifmt.Errorf("job aborted due to lease expiry") case <-ctx.Done(): + // Return context error directly - caller will determine if this is due to graceful shutdown + // or job timeout based on which context was cancelled return ctx.Err() } } -func (d *jobDriver) processJob(ctx context.Context, job *provisioning.Job, recorder JobProgressRecorder) error { +func (d *jobDriver) processJob(ctx context.Context, recorder JobProgressRecorder) error { + ctx, span := tracing.Start(ctx, "provisioning.jobs.process_job") + defer span.End() + logger := logging.FromContext(ctx) + d.mu.Lock() + if d.currentJob == nil { + d.mu.Unlock() + return nil + } + + // Here it's safe to copy as only job spec is used for processing + job := d.currentJob.DeepCopy() + repoName := d.currentJob.Spec.Repository + namespace := d.currentJob.Namespace + d.mu.Unlock() + + span.SetAttributes( + attribute.String("job.repository", repoName), + attribute.String("job.action", string(job.Spec.Action)), + ) + for _, worker := range d.workers { if !worker.IsSupported(ctx, *job) { continue } - repo, err := d.repoGetter.GetRepository(ctx, job.Namespace, job.Spec.Repository) + repo, err := d.repoGetter.GetRepository(ctx, namespace, repoName) if err != nil { - return apifmt.Errorf("failed to get repository '%s': %w", job.Spec.Repository, err) + span.RecordError(err) + return apifmt.Errorf("failed to get repository '%s': %w", repoName, err) } r := repo.Config() if r.DeletionTimestamp != nil && !r.DeletionTimestamp.IsZero() { - logger.Info("repository is marked for deletion, skipping processing job", + logger.Info("repository marked for deletion - skip job", "name", r.Name, "namespace", r.Namespace, "deletionTimestamp", r.DeletionTimestamp, @@ -292,51 +375,76 @@ func (d *jobDriver) processJob(ctx context.Context, job *provisioning.Job, recor return nil } - return worker.Process(ctx, repo, *job, recorder) + err = worker.Process(ctx, repo, *job, recorder) + if err != nil { + span.RecordError(err) + } + return err } - return apifmt.Errorf("no workers were registered to handle the job") + err := apifmt.Errorf("no workers were registered to handle the job") + span.RecordError(err) + return err } -func (d *jobDriver) onProgress(job *provisioning.Job) ProgressFn { +func (d *jobDriver) onProgress() ProgressFn { return func(ctx context.Context, status provisioning.JobStatus) error { + ctx, span := tracing.Start(ctx, "provisioning.jobs.update_progress") + defer span.End() + logging.FromContext(ctx).Debug("job progress", "status", status) const maxRetries = 3 for attempt := 0; attempt < maxRetries; attempt++ { - // Use the current job for the first attempt, fetch fresh for retries - currentJob := job + d.mu.Lock() + if d.currentJob == nil { + d.mu.Unlock() + return nil + } + + // Use the current job for the first attempt; on retry attempts, fetch fresh data from the store to resolve conflicts if attempt > 0 { // Fetch the latest version to resolve conflicts - latest, err := d.store.Get(ctx, job.GetNamespace(), job.GetName()) + latest, err := d.store.Get(ctx, d.currentJob.GetNamespace(), d.currentJob.GetName()) if err != nil { + d.mu.Unlock() if apierrors.IsNotFound(err) { // Job was completed/deleted, nothing to update return nil } return apifmt.Errorf("failed to fetch job for progress update: %w", err) } - currentJob = latest + + *d.currentJob = *latest } + job := d.currentJob // Update status on the current job - currentJob.Status = status - - updated, err := d.store.Update(ctx, currentJob) + job.Status = status + updated, err := d.store.Update(ctx, job) if err != nil { if apierrors.IsConflict(err) && attempt < maxRetries-1 { // Conflict detected, retry with fresh data logging.FromContext(ctx).Debug("progress update conflict, retrying", "attempt", attempt+1) continue } + d.mu.Unlock() return apifmt.Errorf("failed to update job progress: %w", err) } // Update succeeded, update our local copy - *job = *updated + *d.currentJob = *updated + d.mu.Unlock() + + span.SetAttributes( + attribute.String("job.state", string(status.State)), + attribute.Int("attempt", attempt+1), + ) return nil } - return apifmt.Errorf("failed to update job progress after %d attempts", maxRetries) + err := apifmt.Errorf("failed to update job progress after %d attempts", maxRetries) + span.RecordError(err) + return err } } diff --git a/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go b/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go index 3cefca9e5af..45d8572e94a 100644 --- a/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go +++ b/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go @@ -1,12 +1,14 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs import ( context "context" + time "time" + + mock "github.com/stretchr/testify/mock" v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" - mock "github.com/stretchr/testify/mock" ) // MockJobProgressRecorder is an autogenerated mock type for the JobProgressRecorder type @@ -271,6 +273,51 @@ func (_c *MockJobProgressRecorder_SetTotal_Call) RunAndReturn(run func(context.C return _c } +// Started provides a mock function with no fields +func (_m *MockJobProgressRecorder) Started() time.Time { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Started") + } + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +// MockJobProgressRecorder_Started_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Started' +type MockJobProgressRecorder_Started_Call struct { + *mock.Call +} + +// Started is a helper method to define mock.On call +func (_e *MockJobProgressRecorder_Expecter) Started() *MockJobProgressRecorder_Started_Call { + return &MockJobProgressRecorder_Started_Call{Call: _e.mock.On("Started")} +} + +func (_c *MockJobProgressRecorder_Started_Call) Run(run func()) *MockJobProgressRecorder_Started_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockJobProgressRecorder_Started_Call) Return(_a0 time.Time) *MockJobProgressRecorder_Started_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockJobProgressRecorder_Started_Call) RunAndReturn(run func() time.Time) *MockJobProgressRecorder_Started_Call { + _c.Call.Return(run) + return _c +} + // StrictMaxErrors provides a mock function with given fields: maxErrors func (_m *MockJobProgressRecorder) StrictMaxErrors(maxErrors int) { _m.Called(maxErrors) diff --git a/pkg/registry/apis/provisioning/jobs/metrics.go b/pkg/registry/apis/provisioning/jobs/metrics.go index dc946838659..01067feccb3 100644 --- a/pkg/registry/apis/provisioning/jobs/metrics.go +++ b/pkg/registry/apis/provisioning/jobs/metrics.go @@ -1,6 +1,8 @@ package jobs import ( + "time" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" "github.com/prometheus/client_golang/prometheus" ) @@ -9,6 +11,10 @@ type JobMetrics struct { registry prometheus.Registerer processedTotal *prometheus.CounterVec durationHist *prometheus.HistogramVec + + incrementalSyncPhaseDurationHist *prometheus.HistogramVec // phases of incremental sync + fullSyncPhaseDurationHist *prometheus.HistogramVec // phases of full sync + syncDurationHist *prometheus.HistogramVec // total sync durations } type QueueMetrics struct { @@ -72,12 +78,44 @@ func RegisterJobMetrics(registry prometheus.Registerer) JobMetrics { }, []string{"action", "resources_changed_bucket"}, ) - registry.MustRegister(durationHist) + + incrementalSyncPhaseDurationHist := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_jobs_incremental_sync_phase_duration_seconds", + Help: "Duration of job phases for incremental sync", + Buckets: prometheus.ExponentialBucketsRange(0.01, 10*60, 8), // 1ms -> 10m + }, + []string{"phase"}, + ) + registry.MustRegister(incrementalSyncPhaseDurationHist) + + fullSyncPhaseDurationHist := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_jobs_full_sync_phase_duration_seconds", + Help: "Duration of job phases for full sync", + Buckets: prometheus.ExponentialBucketsRange(0.01, 10*60, 8), // 1ms -> 10m + }, + []string{"phase"}, + ) + registry.MustRegister(fullSyncPhaseDurationHist) + + syncDurationHist := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_jobs_sync_duration_seconds", + Help: "Duration of sync (full or incremental)", + Buckets: prometheus.ExponentialBucketsRange(0.01, 10*60, 8), // 1ms -> 10m + }, + []string{"type"}, + ) + registry.MustRegister(syncDurationHist) return JobMetrics{ - registry: registry, - processedTotal: processedTotal, - durationHist: durationHist, + registry: registry, + processedTotal: processedTotal, + durationHist: durationHist, + incrementalSyncPhaseDurationHist: incrementalSyncPhaseDurationHist, + fullSyncPhaseDurationHist: fullSyncPhaseDurationHist, + syncDurationHist: syncDurationHist, } } @@ -90,6 +128,18 @@ func (m *JobMetrics) RecordJob(jobAction string, outcome string, resourceCountCh } } +func (m *JobMetrics) RecordIncrementalSyncPhase(phase IncrementalSyncPhase, duration time.Duration) { + m.incrementalSyncPhaseDurationHist.WithLabelValues(phase.String()).Observe(duration.Seconds()) +} + +func (m *JobMetrics) RecordFullSyncPhase(phase FullSyncPhase, duration time.Duration) { + m.fullSyncPhaseDurationHist.WithLabelValues(phase.String()).Observe(duration.Seconds()) +} + +func (m *JobMetrics) RecordSyncDuration(syncType SyncType, duration time.Duration) { + m.syncDurationHist.WithLabelValues(syncType.String()).Observe(duration.Seconds()) +} + func recordConcurrentDriverMetric(registry prometheus.Registerer, numDrivers int) { concurrentDriver := prometheus.NewGaugeVec( prometheus.GaugeOpts{ @@ -101,3 +151,72 @@ func recordConcurrentDriverMetric(registry prometheus.Registerer, numDrivers int registry.MustRegister(concurrentDriver) concurrentDriver.WithLabelValues().Set(float64(numDrivers)) } + +type SyncType int + +const ( + SyncTypeUnknown SyncType = iota // to prevent zero value being valid + SyncTypeFull + SyncTypeIncremental +) + +func (t SyncType) String() string { + switch t { + case SyncTypeFull: + return "full" + case SyncTypeIncremental: + return "incremental" + default: + return "unknown" + } +} + +type FullSyncPhase int + +const ( + FullSyncPhaseUnknown FullSyncPhase = iota // to prevent zero value being valid + FullSyncPhaseCompare + FullSyncPhaseFileDeletions + FullSyncPhaseFolderDeletions + FullSyncPhaseFolderCreations + FullSyncPhaseFileCreations +) + +func (p FullSyncPhase) String() string { + switch p { + case FullSyncPhaseCompare: + return "compare" + case FullSyncPhaseFileDeletions: + return "file_deletions" + case FullSyncPhaseFolderDeletions: + return "folder_deletions" + case FullSyncPhaseFolderCreations: + return "folder_creations" + case FullSyncPhaseFileCreations: + return "file_creations" + default: + return "unknown" + } +} + +type IncrementalSyncPhase int + +const ( + IncrementalSyncPhaseUnknown IncrementalSyncPhase = iota // to prevent zero value being valid + IncrementalSyncPhaseCompare + IncrementalSyncPhaseApply + IncrementalSyncPhaseCleanup +) + +func (p IncrementalSyncPhase) String() string { + switch p { + case IncrementalSyncPhaseCompare: + return "compare" + case IncrementalSyncPhaseApply: + return "apply" + case IncrementalSyncPhaseCleanup: + return "cleanup" + default: + return "unknown" + } +} diff --git a/pkg/registry/apis/provisioning/jobs/persistentstore.go b/pkg/registry/apis/provisioning/jobs/persistentstore.go index 78494aa82cc..b7c7e47bd35 100644 --- a/pkg/registry/apis/provisioning/jobs/persistentstore.go +++ b/pkg/registry/apis/provisioning/jobs/persistentstore.go @@ -8,6 +8,7 @@ import ( "strconv" "time" + "go.opentelemetry.io/otel/attribute" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -18,6 +19,7 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/prometheus/client_golang/prometheus" ) @@ -100,6 +102,16 @@ func NewJobStore(provisioningClient client.ProvisioningV0alpha1Interface, expiry // If err is not nil, the job and rollback values are always nil. // The err may be ErrNoJobs if there are no jobs to claim. func (s *persistentStore) Claim(ctx context.Context) (job *provisioning.Job, rollback func(), err error) { + ctx, span := tracing.Start(ctx, "provisioning.jobs.claim") + defer func() { + if err != nil && !errors.Is(err, ErrNoJobs) { + span.RecordError(err) + } + span.End() + }() + + logger := logging.FromContext(ctx).With("operation", "claim") + requirement, err := labels.NewRequirement(LabelJobClaim, selection.DoesNotExist, nil) if err != nil { return nil, nil, apifmt.Errorf("could not create requirement: %w", err) @@ -114,9 +126,12 @@ func (s *persistentStore) Claim(ctx context.Context) (job *provisioning.Job, rol } if len(jobs.Items) == 0 { + logger.Debug("no jobs available to claim") return nil, nil, ErrNoJobs } + logger.Debug("found jobs available", "count", len(jobs.Items)) + for _, job := range jobs.Items { if job.Labels == nil { job.Labels = make(map[string]string) @@ -145,6 +160,20 @@ func (s *persistentStore) Claim(ctx context.Context) (job *provisioning.Job, rol return nil, nil, apifmt.Errorf("failed to claim job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) } + logger.Info("job claim complete", + "job", updatedJob.GetName(), + "namespace", updatedJob.GetNamespace(), + "repository", updatedJob.Spec.Repository, + "action", updatedJob.Spec.Action, + ) + + span.SetAttributes( + attribute.String("job.name", updatedJob.GetName()), + attribute.String("job.namespace", updatedJob.GetNamespace()), + attribute.String("job.repository", updatedJob.Spec.Repository), + attribute.String("job.action", string(updatedJob.Spec.Action)), + ) + return updatedJob.DeepCopy(), func() { // Rolling back does not need to care about the parent's cancellation state. // This will also use the parent context (i.e. from the for loop!), ensuring we have permissions to do this. @@ -181,50 +210,99 @@ func (s *persistentStore) Claim(ctx context.Context) (job *provisioning.Job, rol } // We failed to claim any jobs. + logger.Debug("no jobs claimed - all already claimed by others") return nil, nil, ErrNoJobs } // Update saves the job back to the store. func (s *persistentStore) Update(ctx context.Context, job *provisioning.Job) (*provisioning.Job, error) { + ctx, span := tracing.Start(ctx, "provisioning.jobs.update") + defer span.End() + + logger := logging.FromContext(ctx).With( + "operation", "update", + "job", job.GetName(), + "namespace", job.GetNamespace(), + ) + + span.SetAttributes( + attribute.String("job.name", job.GetName()), + attribute.String("job.namespace", job.GetNamespace()), + ) + // Set up the provisioning identity for this namespace ctx, _, err := identity.WithProvisioningIdentity(ctx, job.GetNamespace()) if err != nil { + span.RecordError(err) return nil, apifmt.Errorf("failed to get provisioning identity for '%s': %w", job.GetNamespace(), err) } updatedJob, err := s.client.Jobs(job.GetNamespace()).Update(ctx, job, metav1.UpdateOptions{}) if err != nil { + span.RecordError(err) return nil, apifmt.Errorf("failed to update job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) } + logger.Debug("update job complete") return updatedJob, nil } // Get retrieves a job by name for conflict resolution. func (s *persistentStore) Get(ctx context.Context, namespace, name string) (*provisioning.Job, error) { + ctx, span := tracing.Start(ctx, "provisioning.jobs.get") + defer span.End() + + logger := logging.FromContext(ctx).With( + "operation", "get", + "job", name, + "namespace", namespace, + ) + + span.SetAttributes( + attribute.String("job.name", name), + attribute.String("job.namespace", namespace), + ) + // Set up provisioning identity to access jobs across all namespaces ctx, _, err := identity.WithProvisioningIdentity(ctx, namespace) if err != nil { + span.RecordError(err) return nil, apifmt.Errorf("failed to grant provisioning identity for job lookup: %w", err) } // Use Get to directly fetch the job by name job, err := s.client.Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { + span.RecordError(err) return nil, apifmt.Errorf("failed to get job by name '%s': %w", name, err) } + logger.Debug("get job complete") return job, nil } // Complete marks a job as completed and moves it to the historic job store. // When in the historic store, there is no more claim on the job. func (s *persistentStore) Complete(ctx context.Context, job *provisioning.Job) error { - logger := logging.FromContext(ctx).With("namespace", job.GetNamespace(), "job", job.GetName()) + ctx, span := tracing.Start(ctx, "provisioning.jobs.complete") + defer span.End() + + logger := logging.FromContext(ctx).With( + "operation", "complete", + "namespace", job.GetNamespace(), + "job", job.GetName(), + ) + + span.SetAttributes( + attribute.String("job.name", job.GetName()), + attribute.String("job.namespace", job.GetNamespace()), + attribute.String("job.action", string(job.Spec.Action)), + ) // Set up the provisioning identity for this namespace ctx, _, err := identity.WithProvisioningIdentity(ctx, job.GetNamespace()) if err != nil { + span.RecordError(err) return apifmt.Errorf("failed to get provisioning identity for '%s': %w", job.GetNamespace(), err) } @@ -235,6 +313,7 @@ func (s *persistentStore) Complete(ctx context.Context, job *provisioning.Job) e // This is a best-effort operation; if the job is not in the claimed state, we will still attempt to move it to the historic job store. err = s.client.Jobs(job.GetNamespace()).Delete(ctx, job.GetName(), metav1.DeleteOptions{}) if err != nil { + span.RecordError(err) return apifmt.Errorf("failed to delete job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) } logger.Debug("deleted job from job store") @@ -246,26 +325,44 @@ func (s *persistentStore) Complete(ctx context.Context, job *provisioning.Job) e delete(job.Labels, LabelJobClaim) s.queueMetrics.DecreaseQueueSize(string(job.Spec.Action)) - logger.Debug("job completion done") + logger.Debug("complete job complete") return nil } // RenewLease renews the lease for a claimed job, extending its expiry time. // Returns an error if the lease cannot be renewed (e.g., job was completed or lease expired). func (s *persistentStore) RenewLease(ctx context.Context, job *provisioning.Job) error { + ctx, span := tracing.Start(ctx, "provisioning.jobs.renew_lease") + defer span.End() + + logger := logging.FromContext(ctx).With( + "operation", "renew_lease", + "job", job.GetName(), + "namespace", job.GetNamespace(), + ) + + span.SetAttributes( + attribute.String("job.name", job.GetName()), + attribute.String("job.namespace", job.GetNamespace()), + ) + if job.Labels == nil || job.Labels[LabelJobClaim] == "" { - return apifmt.Errorf("job '%s' in '%s' is not claimed", job.GetName(), job.GetNamespace()) + err := apifmt.Errorf("job '%s' in '%s' is not claimed", job.GetName(), job.GetNamespace()) + span.RecordError(err) + return err } // Set up the provisioning identity for this namespace ctx, _, err := identity.WithProvisioningIdentity(ctx, job.GetNamespace()) if err != nil { + span.RecordError(err) return apifmt.Errorf("failed to get provisioning identity for '%s': %w", job.GetNamespace(), err) } // Fetch the latest version to avoid conflicts latestJob, err := s.client.Jobs(job.GetNamespace()).Get(ctx, job.GetName(), metav1.GetOptions{}) if err != nil { + span.RecordError(err) if apierrors.IsNotFound(err) { return apifmt.Errorf("failed to renew lease for job '%s' in '%s': job no longer exists", job.GetName(), job.GetNamespace()) } @@ -274,7 +371,9 @@ func (s *persistentStore) RenewLease(ctx context.Context, job *provisioning.Job) // Verify we still own the lease if latestJob.Labels == nil || latestJob.Labels[LabelJobClaim] == "" { - return apifmt.Errorf("lease lost for job '%s' in '%s': no longer claimed", job.GetName(), job.GetNamespace()) + err := apifmt.Errorf("lease lost for job '%s' in '%s': no longer claimed", job.GetName(), job.GetNamespace()) + span.RecordError(err) + return err } // Update the claim timestamp to current time @@ -284,85 +383,213 @@ func (s *persistentStore) RenewLease(ctx context.Context, job *provisioning.Job) // Update the job in storage with the latest resource version _, err = s.client.Jobs(job.GetNamespace()).Update(ctx, updatedJob, metav1.UpdateOptions{}) if apierrors.IsConflict(err) { - return apifmt.Errorf("failed to renew lease for job '%s' in '%s': lease conflict", job.GetName(), job.GetNamespace()) + err := apifmt.Errorf("failed to renew lease for job '%s' in '%s': lease conflict", job.GetName(), job.GetNamespace()) + span.RecordError(err) + return err } if apierrors.IsNotFound(err) { - return apifmt.Errorf("failed to renew lease for job '%s' in '%s': job no longer exists", job.GetName(), job.GetNamespace()) + err := apifmt.Errorf("failed to renew lease for job '%s' in '%s': job no longer exists", job.GetName(), job.GetNamespace()) + span.RecordError(err) + return err } if err != nil { + span.RecordError(err) return apifmt.Errorf("failed to renew lease for job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) } // Update the job's claim timestamp and resource version in memory job.Labels[LabelJobClaim] = updatedJob.Labels[LabelJobClaim] job.ResourceVersion = updatedJob.ResourceVersion + + logger.Debug("renew lease complete") return nil } // Cleanup finds jobs with expired leases and marks them as failed. // This replaces the old cleanup mechanism and should be called more frequently. func (s *persistentStore) Cleanup(ctx context.Context) error { - // Set up provisioning identity to access jobs across all namespaces - ctx, _, err := identity.WithProvisioningIdentity(ctx, "*") // "*" grants access to all namespaces - if err != nil { - return apifmt.Errorf("failed to grant provisioning identity for cleanup: %w", err) - } + ctx, span := tracing.Start(ctx, "provisioning.jobs.cleanup") + defer span.End() - // Find jobs with expired leases (older than expiry time) - expiry := s.clock().Add(-s.expiry).UnixMilli() - requirement, err := labels.NewRequirement(LabelJobClaim, selection.LessThan, []string{strconv.FormatInt(expiry, 10)}) - if err != nil { - return apifmt.Errorf("could not create requirement: %w", err) - } + startTime := s.clock() + logger := logging.FromContext(ctx).With("operation", "cleanup") - timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - jobs, err := s.client.Jobs("").List(timeoutCtx, metav1.ListOptions{ - LabelSelector: labels.NewSelector().Add(*requirement).String(), - Limit: 100, // Process in batches - }) - cancel() + // List expired jobs + jobs, err := s.listExpiredJobs(ctx) if err != nil { - return apifmt.Errorf("failed to list jobs with expired leases: %w", err) + span.RecordError(err) + return err } // If no jobs found, cleanup is complete - if len(jobs.Items) == 0 { + if len(jobs) == 0 { + duration := s.clock().Sub(startTime) + logger.Info("cleanup complete - no expired jobs found", "duration", duration) + span.SetAttributes( + attribute.Int("count", 0), + attribute.Int64("duration_ms", duration.Milliseconds()), + ) return nil } - for _, job := range jobs.Items { - // Mark job as failed due to lease expiry and archive it - job := job.DeepCopy() - job.Status.State = provisioning.JobStateError - job.Status.Message = "Job failed due to lease expiry - worker may have crashed or lost connection" + logger.Info("found expired jobs", "count", len(jobs)) - // Set namespace context for the completion - ctx, _, err = identity.WithProvisioningIdentity(ctx, job.GetNamespace()) - if err != nil { - return apifmt.Errorf("failed to get provisioning identity for '%s': %w", job.GetNamespace(), err) - } - - // Use Complete to properly archive the failed job - if err := s.Complete(ctx, job); err != nil { - if apierrors.IsNotFound(err) { - // Job was already completed/deleted by another process - continue - } - return apifmt.Errorf("failed to complete expired job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + // Clean up each expired job + for _, job := range jobs { + if err := s.cleanUpExpiredJob(ctx, job); err != nil { + span.RecordError(err) + return err } } + duration := s.clock().Sub(startTime) + logger.Info("cleanup complete", + "duration", duration, + "count", len(jobs), + ) + + span.SetAttributes( + attribute.Int("count", len(jobs)), + attribute.Int64("duration_ms", duration.Milliseconds()), + ) + + return nil +} + +// listExpiredJobs returns jobs with expired leases. +func (s *persistentStore) listExpiredJobs(ctx context.Context) ([]provisioning.Job, error) { + logger := logging.FromContext(ctx) + + // Set up provisioning identity to access jobs across all namespaces + ctx, _, err := identity.WithProvisioningIdentity(ctx, "*") // "*" grants access to all namespaces + if err != nil { + return nil, apifmt.Errorf("failed to grant provisioning identity for cleanup: %w", err) + } + + // Find jobs with expired leases (older than expiry time) + expiry := s.clock().Add(-s.expiry).UnixMilli() + expiryTime := time.UnixMilli(expiry) + logger.Debug("search for expired jobs", "expiry_threshold", expiryTime.Format(time.RFC3339)) + + requirement, err := labels.NewRequirement(LabelJobClaim, selection.LessThan, []string{strconv.FormatInt(expiry, 10)}) + if err != nil { + return nil, apifmt.Errorf("could not create requirement: %w", err) + } + + listCtx, listSpan := tracing.Start(ctx, "provisioning.jobs.cleanup.list_expired_jobs") + defer listSpan.End() + + listSpan.SetAttributes( + attribute.String("expiry_threshold", expiryTime.Format(time.RFC3339)), + attribute.Int64("expiry_duration_seconds", int64(s.expiry.Seconds())), + ) + + timeoutCtx, cancel := context.WithTimeout(listCtx, 5*time.Second) + defer cancel() + + jobList, err := s.client.Jobs("").List(timeoutCtx, metav1.ListOptions{ + LabelSelector: labels.NewSelector().Add(*requirement).String(), + Limit: 100, // Process in batches + }) + if err != nil { + listSpan.RecordError(err) + return nil, apifmt.Errorf("failed to list jobs with expired leases: %w", err) + } + + listSpan.SetAttributes(attribute.Int("jobs_found", len(jobList.Items))) + return jobList.Items, nil +} + +// cleanUpExpiredJob marks a single expired job as failed and archives it. +func (s *persistentStore) cleanUpExpiredJob(ctx context.Context, job provisioning.Job) error { + // Calculate how long the job has been expired + var expiredFor time.Duration + var claimTimestamp time.Time + if claimTime, exists := job.Labels[LabelJobClaim]; exists { + claimMillis, parseErr := strconv.ParseInt(claimTime, 10, 64) + if parseErr == nil { + claimTimestamp = time.UnixMilli(claimMillis) + expiredFor = s.clock().Sub(claimTimestamp) + } + } + + logger := logging.FromContext(ctx).With( + "job", job.GetName(), + "namespace", job.GetNamespace(), + "repository", job.Spec.Repository, + "action", job.Spec.Action, + "expired_for", expiredFor, + ) + + if !claimTimestamp.IsZero() { + logger = logger.With("claim_time", claimTimestamp.Format(time.RFC3339)) + } + + jobCtx, jobSpan := tracing.Start(ctx, "provisioning.jobs.cleanup.complete_expired_job") + defer jobSpan.End() + + jobSpan.SetAttributes( + attribute.String("job.name", job.GetName()), + attribute.String("job.namespace", job.GetNamespace()), + attribute.String("job.repository", job.Spec.Repository), + attribute.String("job.action", string(job.Spec.Action)), + attribute.String("job.expired_for", expiredFor.String()), + ) + + // Mark job as failed due to lease expiry and archive it + jobCopy := job.DeepCopy() + jobCopy.Status.State = provisioning.JobStateError + jobCopy.Status.Message = "Job failed due to lease expiry - worker may have crashed or lost connection" + + // Set namespace context for the completion + jobCtx, _, err := identity.WithProvisioningIdentity(jobCtx, job.GetNamespace()) + if err != nil { + jobSpan.RecordError(err) + return apifmt.Errorf("failed to get provisioning identity for '%s': %w", job.GetNamespace(), err) + } + + // Use Complete to properly archive the failed job + if err := s.Complete(jobCtx, jobCopy); err != nil { + if apierrors.IsNotFound(err) { + // Job was already completed/deleted by another process - this is expected + logger.Warn("job already completed or deleted by another process") + return nil + } + jobSpan.RecordError(err) + return apifmt.Errorf("failed to complete expired job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) + } + + logger.Info("clean up expired job complete") return nil } func (s *persistentStore) Insert(ctx context.Context, namespace string, spec provisioning.JobSpec) (*provisioning.Job, error) { + ctx, span := tracing.Start(ctx, "provisioning.jobs.insert") + defer span.End() + + logger := logging.FromContext(ctx).With( + "operation", "insert", + "namespace", namespace, + "repository", spec.Repository, + "action", spec.Action, + ) + + span.SetAttributes( + attribute.String("job.namespace", namespace), + attribute.String("job.repository", spec.Repository), + attribute.String("job.action", string(spec.Action)), + ) + if spec.Repository == "" { - return nil, errors.New("missing repository in job") + err := errors.New("missing repository in job") + span.RecordError(err) + return nil, err } // Set up the provisioning identity for this namespace ctx, _, err := identity.WithProvisioningIdentity(ctx, namespace) if err != nil { + span.RecordError(err) return nil, apifmt.Errorf("failed to get provisioning identity for '%s': %w", namespace, err) } @@ -376,19 +603,27 @@ func (s *persistentStore) Insert(ctx context.Context, namespace string, spec pro Spec: spec, } if err := mutateJobAction(job); err != nil { + span.RecordError(err) return nil, err } generateJobName(job) // Side-effect: updates the job's name. + + logger = logger.With("job", job.GetName()) + span.SetAttributes(attribute.String("job.name", job.GetName())) + created, err := s.client.Jobs(namespace).Create(ctx, job, metav1.CreateOptions{}) if apierrors.IsAlreadyExists(err) { + span.RecordError(err) return nil, apifmt.Errorf("job '%s' in '%s' already exists: %w", job.GetName(), job.GetNamespace(), err) } if err != nil { + span.RecordError(err) return nil, apifmt.Errorf("failed to create job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) } s.queueMetrics.IncreaseQueueSize(string(job.Spec.Action)) + logger.Info("insert job complete") return created, nil } diff --git a/pkg/registry/apis/provisioning/jobs/progress.go b/pkg/registry/apis/provisioning/jobs/progress.go index f9cea399865..97a293a5d8c 100644 --- a/pkg/registry/apis/provisioning/jobs/progress.go +++ b/pkg/registry/apis/provisioning/jobs/progress.go @@ -69,24 +69,36 @@ func newJobProgressRecorder(ProgressFn ProgressFn) JobProgressRecorder { } } +func (r *jobProgressRecorder) Started() time.Time { + return r.started +} + func (r *jobProgressRecorder) Record(ctx context.Context, result JobResourceResult) { + var shouldLogError bool + var logErr error + r.mu.Lock() r.resultCount++ - logger := logging.FromContext(ctx).With("path", result.Path, "group", result.Group, "kind", result.Kind, "action", result.Action, "name", result.Name) if result.Error != nil { - logger.Error("job resource operation failed", "err", result.Error) + shouldLogError = true + logErr = result.Error if len(r.errors) < 20 { r.errors = append(r.errors, result.Error.Error()) } r.errorCount++ - } else { - logger.Info("job resource operation succeeded") } r.updateSummary(result) r.mu.Unlock() + logger := logging.FromContext(ctx).With("path", result.Path, "group", result.Group, "kind", result.Kind, "action", result.Action, "name", result.Name) + if shouldLogError { + logger.Error("job resource operation failed", "err", logErr) + } else { + logger.Info("job resource operation succeeded") + } + r.maybeNotify(ctx) } @@ -145,9 +157,6 @@ func (r *jobProgressRecorder) StrictMaxErrors(maxErrors int) { } func (r *jobProgressRecorder) TooManyErrors() error { - r.mu.RLock() - defer r.mu.RUnlock() - if r.maxErrors > 0 && r.errorCount >= r.maxErrors { return fmt.Errorf("too many errors: %d", r.errorCount) } @@ -156,9 +165,6 @@ func (r *jobProgressRecorder) TooManyErrors() error { } func (r *jobProgressRecorder) summary() []*provisioning.JobResourceSummary { - r.mu.RLock() - defer r.mu.RUnlock() - if len(r.summaries) == 0 { return nil } @@ -247,13 +253,9 @@ func (r *jobProgressRecorder) maybeNotify(ctx context.Context) { func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provisioning.JobStatus { r.mu.RLock() - defer r.mu.RUnlock() - // Initialize base job status jobStatus := provisioning.JobStatus{ - Started: r.started.UnixMilli(), - // FIXME: if we call this method twice, the state will be different - // This results in sync status to be different from job status + Started: r.started.UnixMilli(), Finished: time.Now().UnixMilli(), State: provisioning.JobStateSuccess, Message: "completed successfully", @@ -268,9 +270,13 @@ func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provision jobStatus.Errors = r.errors jobStatus.URLs = r.refURLs - // Check for errors during execution + tooManyErrors := r.maxErrors > 0 && r.errorCount >= r.maxErrors + finalMessage := r.finalMessage + + r.mu.RUnlock() + if len(jobStatus.Errors) > 0 && jobStatus.State != provisioning.JobStateError { - if r.TooManyErrors() != nil { + if tooManyErrors { jobStatus.Message = "completed with too many errors" jobStatus.State = provisioning.JobStateError } else { @@ -280,8 +286,8 @@ func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provision } // Override message if progress have a more explicit message - if r.finalMessage != "" && jobStatus.State != provisioning.JobStateError { - jobStatus.Message = r.finalMessage + if finalMessage != "" && jobStatus.State != provisioning.JobStateError { + jobStatus.Message = finalMessage } return jobStatus diff --git a/pkg/registry/apis/provisioning/jobs/queue.go b/pkg/registry/apis/provisioning/jobs/queue.go index 14f6b8bd9c7..e1992395efd 100644 --- a/pkg/registry/apis/provisioning/jobs/queue.go +++ b/pkg/registry/apis/provisioning/jobs/queue.go @@ -2,6 +2,7 @@ package jobs import ( "context" + "time" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" @@ -18,6 +19,7 @@ type RepoGetter interface { // //go:generate mockery --name JobProgressRecorder --structname MockJobProgressRecorder --inpackage --filename job_progress_recorder_mock.go --with-expecter type JobProgressRecorder interface { + Started() time.Time Record(ctx context.Context, result JobResourceResult) ResetResults() SetFinalMessage(ctx context.Context, msg string) diff --git a/pkg/registry/apis/provisioning/jobs/sync/full.go b/pkg/registry/apis/provisioning/jobs/sync/full.go index 0c5d9c5bb2d..10aad46693b 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/full.go +++ b/pkg/registry/apis/provisioning/jobs/sync/full.go @@ -4,12 +4,14 @@ import ( "context" "fmt" "sync" + "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/apps/provisioning/pkg/safepath" "github.com/grafana/grafana/pkg/infra/tracing" @@ -27,11 +29,16 @@ func FullSync( progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int, + metrics jobs.JobMetrics, ) error { + syncStart := time.Now() cfg := repo.Config() ctx, span := tracer.Start(ctx, "provisioning.sync.full") defer span.End() + defer func() { + metrics.RecordSyncDuration(jobs.SyncTypeFull, time.Since(syncStart)) + }() ensureFolderCtx, ensureFolderSpan := tracer.Start(ctx, "provisioning.sync.full.ensure_folder_exists") // Ensure the configured folder exists and is managed by the repository @@ -49,19 +56,23 @@ func FullSync( ensureFolderSpan.End() compareCtx, compareSpan := tracer.Start(ctx, "provisioning.sync.full.compare") - changes, err := compare(compareCtx, repo, repositoryResources, currentRef) + var changes []ResourceFileChange + err := instrumentedFullSyncPhase(jobs.FullSyncPhaseCompare, func() (err error) { + changes, err = compare(compareCtx, repo, repositoryResources, currentRef) + return + }, metrics) + compareSpan.End() + if err != nil { - compareSpan.End() return tracing.Error(span, fmt.Errorf("compare changes: %w", err)) } - compareSpan.End() if len(changes) == 0 { progress.SetFinalMessage(ctx, "no changes to sync") return nil } - return applyChanges(ctx, changes, clients, repositoryResources, progress, tracer, maxSyncWorkers) + return applyChanges(ctx, changes, clients, repositoryResources, progress, tracer, maxSyncWorkers, metrics) } func applyChange(ctx context.Context, change ResourceFileChange, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) { @@ -145,16 +156,24 @@ func applyChange(ctx context.Context, change ResourceFileChange, clients resourc Group: gvk.Group, Kind: gvk.Kind, } - if err != nil { writeSpan.RecordError(err) result.Error = fmt.Errorf("writing resource from file %s: %w", change.Path, err) } + progress.Record(writeCtx, result) writeSpan.End() } -func applyChanges(ctx context.Context, changes []ResourceFileChange, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int) error { +// instrument a function with a phase and metrics +func instrumentedFullSyncPhase(phase jobs.FullSyncPhase, fn func() error, metrics jobs.JobMetrics) error { + phaseStart := time.Now() + err := fn() + metrics.RecordFullSyncPhase(phase, time.Since(phaseStart)) + return err +} + +func applyChanges(ctx context.Context, changes []ResourceFileChange, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int, metrics jobs.JobMetrics) error { progress.SetTotal(ctx, len(changes)) _, applyChangesSpan := tracer.Start(ctx, "provisioning.sync.full.apply_changes", @@ -199,97 +218,110 @@ func applyChanges(ctx context.Context, changes []ResourceFileChange, clients res ) if len(fileDeletions) > 0 { - if err := applyResourcesInParallel(ctx, fileDeletions, clients, repositoryResources, progress, tracer, maxSyncWorkers); err != nil { + if err := instrumentedFullSyncPhase(jobs.FullSyncPhaseFileDeletions, func() error { + return applyResourcesInParallel(ctx, fileDeletions, clients, repositoryResources, progress, tracer, maxSyncWorkers) + }, metrics); err != nil { return err } } if len(folderDeletions) > 0 { - if err := applyFoldersSerially(ctx, folderDeletions, clients, repositoryResources, progress, tracer); err != nil { + if err := instrumentedFullSyncPhase(jobs.FullSyncPhaseFolderDeletions, func() error { + return applyFoldersSerially(ctx, folderDeletions, clients, repositoryResources, progress, tracer) + }, metrics); err != nil { return err } } if len(folderCreations) > 0 { - if err := applyFoldersSerially(ctx, folderCreations, clients, repositoryResources, progress, tracer); err != nil { + if err := instrumentedFullSyncPhase(jobs.FullSyncPhaseFolderCreations, func() error { + return applyFoldersSerially(ctx, folderCreations, clients, repositoryResources, progress, tracer) + }, metrics); err != nil { return err } } if len(fileCreations) > 0 { - return applyResourcesInParallel(ctx, fileCreations, clients, repositoryResources, progress, tracer, maxSyncWorkers) + if err := instrumentedFullSyncPhase(jobs.FullSyncPhaseFileCreations, func() error { + return applyResourcesInParallel(ctx, fileCreations, clients, repositoryResources, progress, tracer, maxSyncWorkers) + }, metrics); err != nil { + return err + } } return nil } func applyFoldersSerially(ctx context.Context, folders []ResourceFileChange, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error { - folderCtx, folderCancel := context.WithCancel(ctx) - defer folderCancel() + logger := logging.FromContext(ctx) for _, folder := range folders { - if folderCtx.Err() != nil { - return folderCtx.Err() + if ctx.Err() != nil { + return ctx.Err() } if err := progress.TooManyErrors(); err != nil { return err } + folderCtx, cancel := context.WithTimeout(ctx, 15*time.Second) + applyChange(folderCtx, folder, clients, repositoryResources, progress, tracer) + + if folderCtx.Err() == context.DeadlineExceeded { + logger.Error("operation timed out after 15 seconds", "path", folder.Path, "action", folder.Action) + + recordCtx, recordCancel := context.WithTimeout(context.Background(), 15*time.Second) + progress.Record(recordCtx, jobs.JobResourceResult{ + Path: folder.Path, + Action: folder.Action, + Error: fmt.Errorf("operation timed out after 15 seconds"), + }) + recordCancel() + } + + cancel() } return nil } func applyResourcesInParallel(ctx context.Context, resources []ResourceFileChange, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int) error { + logger := logging.FromContext(ctx) + logger.Info("applying resources in parallel test changes 1") + if len(resources) == 0 { return nil } - workerCtx, cancel := context.WithCancel(ctx) - defer cancel() - - changeChan := make(chan ResourceFileChange, len(resources)) + sem := make(chan struct{}, maxSyncWorkers) var wg sync.WaitGroup - for i := 0; i < maxSyncWorkers; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for { - select { - case change, ok := <-changeChan: - if !ok { - return - } - - if err := progress.TooManyErrors(); err != nil { - cancel() - return - } - if workerCtx.Err() != nil { - return - } - - applyChange(workerCtx, change, clients, repositoryResources, progress, tracer) - - case <-workerCtx.Done(): - return - } - } - }() - } - +loop: for _, change := range resources { - select { - case changeChan <- change: - case <-workerCtx.Done(): - goto done + if err := progress.TooManyErrors(); err != nil { + break } + if ctx.Err() != nil { + break + } + + // Acquire semaphore slot (blocks if max workers reached) + select { + case sem <- struct{}{}: + case <-ctx.Done(): + break loop + } + + wg.Add(1) + go func(change ResourceFileChange) { + defer wg.Done() + defer func() { <-sem }() + + applyChangeWithTimeout(ctx, change, clients, repositoryResources, progress, tracer, logger) + }(change) } -done: - close(changeChan) + wg.Wait() if err := progress.TooManyErrors(); err != nil { @@ -298,3 +330,22 @@ done: return ctx.Err() } + +func applyChangeWithTimeout(ctx context.Context, change ResourceFileChange, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, logger logging.Logger) { + changeCtx, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + + applyChange(changeCtx, change, clients, repositoryResources, progress, tracer) + + if changeCtx.Err() == context.DeadlineExceeded { + logger.Error("operation timed out after 15 seconds", "path", change.Path, "action", change.Action) + + recordCtx, recordCancel := context.WithTimeout(context.Background(), 15*time.Second) + progress.Record(recordCtx, jobs.JobResourceResult{ + Path: change.Path, + Action: change.Action, + Error: fmt.Errorf("operation timed out after 15 seconds"), + }) + recordCancel() + } +} diff --git a/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go index c3e0a68a8c6..a9b18943898 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go @@ -28,17 +28,17 @@ func (_m *MockFullSyncFn) EXPECT() *MockFullSyncFn_Expecter { return &MockFullSyncFn_Expecter{mock: &_m.Mock} } -// Execute provides a mock function with given fields: ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer -func (_m *MockFullSyncFn) Execute(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int) error { - ret := _m.Called(ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer) +// Execute provides a mock function with given fields: ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer, maxSyncWorkers, metrics +func (_m *MockFullSyncFn) Execute(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int, metrics jobs.JobMetrics) error { + ret := _m.Called(ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer, maxSyncWorkers, metrics) if len(ret) == 0 { panic("no return value specified for Execute") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.Reader, CompareFn, resources.ResourceClients, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer) error); ok { - r0 = rf(ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer) + if rf, ok := ret.Get(0).(func(context.Context, repository.Reader, CompareFn, resources.ResourceClients, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer, int, jobs.JobMetrics) error); ok { + r0 = rf(ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer, maxSyncWorkers, metrics) } else { r0 = ret.Error(0) } @@ -60,13 +60,15 @@ type MockFullSyncFn_Execute_Call struct { // - repositoryResources resources.RepositoryResources // - progress jobs.JobProgressRecorder // - tracer tracing.Tracer -func (_e *MockFullSyncFn_Expecter) Execute(ctx interface{}, repo interface{}, compare interface{}, clients interface{}, currentRef interface{}, repositoryResources interface{}, progress interface{}, tracer interface{}) *MockFullSyncFn_Execute_Call { - return &MockFullSyncFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer)} +// - maxSyncWorkers int +// - metrics jobs.JobMetrics +func (_e *MockFullSyncFn_Expecter) Execute(ctx interface{}, repo interface{}, compare interface{}, clients interface{}, currentRef interface{}, repositoryResources interface{}, progress interface{}, tracer interface{}, maxSyncWorkers interface{}, metrics interface{}) *MockFullSyncFn_Execute_Call { + return &MockFullSyncFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer, maxSyncWorkers, metrics)} } -func (_c *MockFullSyncFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer)) *MockFullSyncFn_Execute_Call { +func (_c *MockFullSyncFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int, metrics jobs.JobMetrics)) *MockFullSyncFn_Execute_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.Reader), args[2].(CompareFn), args[3].(resources.ResourceClients), args[4].(string), args[5].(resources.RepositoryResources), args[6].(jobs.JobProgressRecorder), args[7].(tracing.Tracer)) + run(args[0].(context.Context), args[1].(repository.Reader), args[2].(CompareFn), args[3].(resources.ResourceClients), args[4].(string), args[5].(resources.RepositoryResources), args[6].(jobs.JobProgressRecorder), args[7].(tracing.Tracer), args[8].(int), args[9].(jobs.JobMetrics)) }) return _c } @@ -76,7 +78,7 @@ func (_c *MockFullSyncFn_Execute_Call) Return(_a0 error) *MockFullSyncFn_Execute return _c } -func (_c *MockFullSyncFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Reader, CompareFn, resources.ResourceClients, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer) error) *MockFullSyncFn_Execute_Call { +func (_c *MockFullSyncFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Reader, CompareFn, resources.ResourceClients, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer, int, jobs.JobMetrics) error) *MockFullSyncFn_Execute_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/jobs/sync/full_test.go b/pkg/registry/apis/provisioning/jobs/sync/full_test.go index 76cf514f23d..aaa61ee61db 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/full_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/full_test.go @@ -4,8 +4,11 @@ import ( "context" "errors" "fmt" + "sync/atomic" "testing" + "time" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,7 +46,7 @@ func TestFullSync_ContextCancelled(t *testing.T) { compareFn.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]ResourceFileChange{{}}, nil) progress.On("SetTotal", mock.Anything, 1).Return() - err := FullSync(ctx, repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10) + err := FullSync(ctx, repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) require.EqualError(t, err, "context canceled") } @@ -62,7 +65,7 @@ func TestFullSync_Error(t *testing.T) { compareFn.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("some error")) - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) require.EqualError(t, err, "compare changes: some error") } @@ -82,7 +85,7 @@ func TestFullSync_NoChanges(t *testing.T) { compareFn.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]ResourceFileChange{}, nil) progress.On("SetFinalMessage", mock.Anything, "no changes to sync").Return() - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) require.NoError(t, err) } @@ -113,7 +116,7 @@ func TestFullSync_SuccessfulFolderCreation(t *testing.T) { Path: "", }, "").Return(nil) - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) require.NoError(t, err) } @@ -142,7 +145,7 @@ func TestFullSync_FolderCreationFailed(t *testing.T) { Path: "", }, "").Return(fmt.Errorf("folder creation failed")) - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) require.Error(t, err) require.Contains(t, err.Error(), "create root folder: folder creation failed") } @@ -171,7 +174,7 @@ func TestFullSync_FolderCreationFailedWithInstanceTarget(t *testing.T) { compareFn.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(nil, fmt.Errorf("compare error")) - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) require.Error(t, err) require.Contains(t, err.Error(), "compare changes: compare error") } @@ -202,10 +205,9 @@ func TestFullSync_ApplyChanges(t *testing.T) { //nolint:gocyclo }, }, setupMocks: func(repo *repository.MockRepository, repoResources *resources.MockRepositoryResources, clients *resources.MockResourceClients, progress *jobs.MockJobProgressRecorder, compareFn *MockCompareFn) { - callCount := 0 + var callCount int64 = 0 progress.On("TooManyErrors").Return(func() error { - callCount++ - if callCount > 1 { + if atomic.AddInt64(&callCount, 1) > 1 { return fmt.Errorf("too many errors") } return nil @@ -682,6 +684,45 @@ func TestFullSync_ApplyChanges(t *testing.T) { //nolint:gocyclo })).Return() }, }, + { + name: "operation timeout after 15 seconds", + description: "Should record timeout error when operation takes longer than 15 seconds", + changes: []ResourceFileChange{ + { + Action: repository.FileActionCreated, + Path: "dashboards/slow.json", + }, + }, + setupMocks: func(repo *repository.MockRepository, repoResources *resources.MockRepositoryResources, clients *resources.MockResourceClients, progress *jobs.MockJobProgressRecorder, compareFn *MockCompareFn) { + progress.On("TooManyErrors").Return(nil) + + repoResources.On("WriteResourceFromFile", mock.Anything, "dashboards/slow.json", ""). + Run(func(args mock.Arguments) { + ctx := args.Get(0).(context.Context) + select { + case <-ctx.Done(): + return + case <-time.After(20 * time.Second): + return + } + }). + Return("", schema.GroupVersionKind{}, context.DeadlineExceeded) + + progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Action == repository.FileActionCreated && + result.Path == "dashboards/slow.json" && + result.Error != nil && + result.Error.Error() == "writing resource from file dashboards/slow.json: context deadline exceeded" + })).Return().Once() + + progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { + return result.Action == repository.FileActionCreated && + result.Path == "dashboards/slow.json" && + result.Error != nil && + result.Error.Error() == "operation timed out after 15 seconds" + })).Return().Once() + }, + }, } for _, tt := range tests { @@ -704,7 +745,7 @@ func TestFullSync_ApplyChanges(t *testing.T) { //nolint:gocyclo }) progress.On("SetTotal", mock.Anything, len(tt.changes)).Return() - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService(), 10, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) if tt.expectedError != "" { require.EqualError(t, err, tt.expectedError, tt.description) } else { diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental.go b/pkg/registry/apis/provisioning/jobs/sync/incremental.go index 0c4ddde659b..daa94d94636 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/apps/provisioning/pkg/safepath" @@ -16,7 +17,8 @@ import ( ) // Convert git changes into resource file changes -func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error { +func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, metrics jobs.JobMetrics) error { + syncStart := time.Now() if previousRef == currentRef { progress.SetFinalMessage(ctx, "same commit as last time") return nil @@ -24,7 +26,11 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef ctx, span := tracer.Start(ctx, "provisioning.sync.incremental") defer span.End() + defer func() { + metrics.RecordSyncDuration(jobs.SyncTypeIncremental, time.Since(syncStart)) + }() + compareStart := time.Now() compareCtx, compareSpan := tracer.Start(ctx, "provisioning.sync.incremental.compare_files") diff, err := repo.CompareFiles(compareCtx, previousRef, currentRef) if err != nil { @@ -33,6 +39,7 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef return tracing.Error(span, fmt.Errorf("compare files error: %w", err)) } compareSpan.End() + metrics.RecordIncrementalSyncPhase(jobs.IncrementalSyncPhaseCompare, time.Since(compareStart)) if len(diff) < 1 { progress.SetFinalMessage(ctx, "no changes detected between commits") @@ -41,18 +48,41 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef progress.SetTotal(ctx, len(diff)) progress.SetMessage(ctx, "replicating versioned changes") + applyStart := time.Now() + affectedFolders, err := applyIncrementalChanges(ctx, diff, repositoryResources, progress, tracer, span) + metrics.RecordIncrementalSyncPhase(jobs.IncrementalSyncPhaseApply, time.Since(applyStart)) + if err != nil { + return err + } + progress.SetMessage(ctx, "versioned changes replicated") + + if len(affectedFolders) > 0 { + cleanupStart := time.Now() + span.AddEvent("checking if impacted folders should be deleted", trace.WithAttributes(attribute.Int("affected_folders", len(affectedFolders)))) + err := cleanupOrphanedFolders(ctx, repo, affectedFolders, repositoryResources, tracer) + metrics.RecordIncrementalSyncPhase(jobs.IncrementalSyncPhaseCleanup, time.Since(cleanupStart)) + if err != nil { + return tracing.Error(span, fmt.Errorf("cleanup orphaned folders: %w", err)) + } + } + + return nil +} + +func applyIncrementalChanges(ctx context.Context, diff []repository.VersionedFileChange, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, span trace.Span) (affectedFolders map[string]string, err error) { // this will keep track of any folders that had resources deleted from it // with key-value as path:grafana uid. // after cleaning up all resources, we will look to see if the foldrs are // now empty, and if so, delete them. - affectedFolders := make(map[string]string) + affectedFolders = make(map[string]string) + for _, change := range diff { if ctx.Err() != nil { - return ctx.Err() + return nil, ctx.Err() } if err := progress.TooManyErrors(); err != nil { - return tracing.Error(span, err) + return nil, tracing.Error(span, err) } if err := resources.IsPathSupported(change.Path); err != nil { @@ -68,7 +98,7 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef if err != nil { ensureFolderSpan.RecordError(err) ensureFolderSpan.End() - return tracing.Error(span, fmt.Errorf("unable to create empty file folder: %w", err)) + return nil, tracing.Error(span, fmt.Errorf("unable to create empty file folder: %w", err)) } progress.Record(ensureFolderCtx, jobs.JobResourceResult{ @@ -145,16 +175,7 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef progress.Record(ctx, result) } - progress.SetMessage(ctx, "versioned changes replicated") - - if len(affectedFolders) > 0 { - span.AddEvent("checking if impacted folders should be deleted", trace.WithAttributes(attribute.Int("affected_folders", len(affectedFolders)))) - if err := cleanupOrphanedFolders(ctx, repo, affectedFolders, repositoryResources, tracer); err != nil { - return tracing.Error(span, fmt.Errorf("cleanup orphaned folders: %w", err)) - } - } - - return nil + return affectedFolders, nil } // cleanupOrphanedFolders removes folders that no longer contain any resources in git after deletions have occurred. diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go index ff032146788..417f8d89efe 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go @@ -28,17 +28,17 @@ func (_m *MockIncrementalSyncFn) EXPECT() *MockIncrementalSyncFn_Expecter { return &MockIncrementalSyncFn_Expecter{mock: &_m.Mock} } -// Execute provides a mock function with given fields: ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer -func (_m *MockIncrementalSyncFn) Execute(ctx context.Context, repo repository.Versioned, previousRef string, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error { - ret := _m.Called(ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer) +// Execute provides a mock function with given fields: ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer, metrics +func (_m *MockIncrementalSyncFn) Execute(ctx context.Context, repo repository.Versioned, previousRef string, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, metrics jobs.JobMetrics) error { + ret := _m.Called(ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer, metrics) if len(ret) == 0 { panic("no return value specified for Execute") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.Versioned, string, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer) error); ok { - r0 = rf(ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer) + if rf, ok := ret.Get(0).(func(context.Context, repository.Versioned, string, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer, jobs.JobMetrics) error); ok { + r0 = rf(ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer, metrics) } else { r0 = ret.Error(0) } @@ -59,13 +59,14 @@ type MockIncrementalSyncFn_Execute_Call struct { // - repositoryResources resources.RepositoryResources // - progress jobs.JobProgressRecorder // - tracer tracing.Tracer -func (_e *MockIncrementalSyncFn_Expecter) Execute(ctx interface{}, repo interface{}, previousRef interface{}, currentRef interface{}, repositoryResources interface{}, progress interface{}, tracer interface{}) *MockIncrementalSyncFn_Execute_Call { - return &MockIncrementalSyncFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer)} +// - metrics jobs.JobMetrics +func (_e *MockIncrementalSyncFn_Expecter) Execute(ctx interface{}, repo interface{}, previousRef interface{}, currentRef interface{}, repositoryResources interface{}, progress interface{}, tracer interface{}, metrics interface{}) *MockIncrementalSyncFn_Execute_Call { + return &MockIncrementalSyncFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer, metrics)} } -func (_c *MockIncrementalSyncFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Versioned, previousRef string, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer)) *MockIncrementalSyncFn_Execute_Call { +func (_c *MockIncrementalSyncFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Versioned, previousRef string, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, metrics jobs.JobMetrics)) *MockIncrementalSyncFn_Execute_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.Versioned), args[2].(string), args[3].(string), args[4].(resources.RepositoryResources), args[5].(jobs.JobProgressRecorder), args[6].(tracing.Tracer)) + run(args[0].(context.Context), args[1].(repository.Versioned), args[2].(string), args[3].(string), args[4].(resources.RepositoryResources), args[5].(jobs.JobProgressRecorder), args[6].(tracing.Tracer), args[7].(jobs.JobMetrics)) }) return _c } @@ -75,7 +76,7 @@ func (_c *MockIncrementalSyncFn_Execute_Call) Return(_a0 error) *MockIncremental return _c } -func (_c *MockIncrementalSyncFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Versioned, string, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer) error) *MockIncrementalSyncFn_Execute_Call { +func (_c *MockIncrementalSyncFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Versioned, string, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer, jobs.JobMetrics) error) *MockIncrementalSyncFn_Execute_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go b/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go index 95d595e8c6c..f694d7f5068 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" @@ -31,7 +32,7 @@ func TestIncrementalSync_ContextCancelled(t *testing.T) { progress.On("SetTotal", mock.Anything, 1).Return() progress.On("SetMessage", mock.Anything, "replicating versioned changes").Return() - err := IncrementalSync(ctx, repo, "old-ref", "new-ref", repoResources, progress, tracing.NewNoopTracerService()) + err := IncrementalSync(ctx, repo, "old-ref", "new-ref", repoResources, progress, tracing.NewNoopTracerService(), jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) require.EqualError(t, err, "context canceled") } @@ -388,7 +389,7 @@ func TestIncrementalSync(t *testing.T) { tt.setupMocks(repo, repoResources, progress) - err := IncrementalSync(context.Background(), repo, tt.previousRef, tt.currentRef, repoResources, progress, tracing.NewNoopTracerService()) + err := IncrementalSync(context.Background(), repo, tt.previousRef, tt.currentRef, repoResources, progress, tracing.NewNoopTracerService(), jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) if tt.expectedError != "" { require.EqualError(t, err, tt.expectedError) @@ -511,7 +512,7 @@ func TestIncrementalSync_CleanupOrphanedFolders(t *testing.T) { tt.setupMocks(repo, repoResources, progress) - err := IncrementalSync(context.Background(), repo, "old-ref", "new-ref", repoResources, progress, tracing.NewNoopTracerService()) + err := IncrementalSync(context.Background(), repo, "old-ref", "new-ref", repoResources, progress, tracing.NewNoopTracerService(), jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) if tt.expectedError != "" { require.EqualError(t, err, tt.expectedError) diff --git a/pkg/registry/apis/provisioning/jobs/sync/sync.go b/pkg/registry/apis/provisioning/jobs/sync/sync.go index 55272332add..7f677a8ce3b 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/sync.go +++ b/pkg/registry/apis/provisioning/jobs/sync/sync.go @@ -12,13 +12,13 @@ import ( ) //go:generate mockery --name FullSyncFn --structname MockFullSyncFn --inpackage --filename full_sync_fn_mock.go --with-expecter -type FullSyncFn func(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int) error +type FullSyncFn func(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, maxSyncWorkers int, metrics jobs.JobMetrics) error //go:generate mockery --name CompareFn --structname MockCompareFn --inpackage --filename compare_fn_mock.go --with-expecter type CompareFn func(ctx context.Context, repo repository.Reader, repositoryResources resources.RepositoryResources, ref string) ([]ResourceFileChange, error) //go:generate mockery --name IncrementalSyncFn --structname MockIncrementalSyncFn --inpackage --filename incremental_sync_fn_mock.go --with-expecter -type IncrementalSyncFn func(ctx context.Context, repo repository.Versioned, previousRef, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error +type IncrementalSyncFn func(ctx context.Context, repo repository.Versioned, previousRef, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer, metrics jobs.JobMetrics) error //go:generate mockery --name Syncer --structname MockSyncer --inpackage --filename syncer_mock.go --with-expecter type Syncer interface { @@ -30,15 +30,17 @@ type syncer struct { fullSync FullSyncFn incrementalSync IncrementalSyncFn tracer tracing.Tracer + metrics jobs.JobMetrics maxSyncWorkers int } -func NewSyncer(compare CompareFn, fullSync FullSyncFn, incrementalSync IncrementalSyncFn, tracer tracing.Tracer, maxSyncWorkers int) Syncer { +func NewSyncer(compare CompareFn, fullSync FullSyncFn, incrementalSync IncrementalSyncFn, tracer tracing.Tracer, maxSyncWorkers int, metrics jobs.JobMetrics) Syncer { return &syncer{ compare: compare, fullSync: fullSync, incrementalSync: incrementalSync, tracer: tracer, + metrics: metrics, maxSyncWorkers: maxSyncWorkers, } } @@ -57,11 +59,11 @@ func (r *syncer) Sync(ctx context.Context, repo repository.ReaderWriter, options if cfg.Status.Sync.LastRef != "" && options.Incremental { progress.SetMessage(ctx, "incremental sync") - return currentRef, r.incrementalSync(ctx, versionedRepo, cfg.Status.Sync.LastRef, currentRef, repositoryResources, progress, r.tracer) + return currentRef, r.incrementalSync(ctx, versionedRepo, cfg.Status.Sync.LastRef, currentRef, repositoryResources, progress, r.tracer, r.metrics) } } progress.SetMessage(ctx, "full sync") - return currentRef, r.fullSync(ctx, repo, r.compare, clients, currentRef, repositoryResources, progress, r.tracer, r.maxSyncWorkers) + return currentRef, r.fullSync(ctx, repo, r.compare, clients, currentRef, repositoryResources, progress, r.tracer, r.maxSyncWorkers, r.metrics) } diff --git a/pkg/registry/apis/provisioning/jobs/sync/sync_test.go b/pkg/registry/apis/provisioning/jobs/sync/sync_test.go index e0d1bbee7e1..d8c40689874 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/sync_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/sync_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/prometheus/client_golang/prometheus" mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -66,7 +67,7 @@ func TestSyncer_Sync(t *testing.T) { repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil) progress.On("SetMessage", mock.Anything, "full sync").Return() - fullSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, mock.Anything, mock.Anything, "new-ref", mock.Anything, mock.Anything, mock.Anything).Return(nil) + fullSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, mock.Anything, mock.Anything, "new-ref", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) }, expectedMessages: []string{"full sync"}, }, @@ -88,7 +89,7 @@ func TestSyncer_Sync(t *testing.T) { }) repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil) progress.On("SetMessage", mock.Anything, "incremental sync").Return() - incrementalSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything, mock.Anything).Return(nil) + incrementalSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) }, expectedRef: "new-ref", expectedMessages: []string{"incremental sync"}, @@ -131,7 +132,7 @@ func TestSyncer_Sync(t *testing.T) { }) repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil) progress.On("SetMessage", mock.Anything, "incremental sync").Return() - incrementalSyncFn.On("Execute", mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("incremental sync failed")) + incrementalSyncFn.On("Execute", mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("incremental sync failed")) }, expectedRef: "new-ref", expectedMessages: []string{"incremental sync"}, @@ -161,6 +162,7 @@ func TestSyncer_Sync(t *testing.T) { incrementalSyncFn.Execute, tracing.NewNoopTracerService(), 10, + jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()), ) ref, err := syncer.Sync(context.Background(), repo, tt.options, repoResources, clients, progress) diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker.go b/pkg/registry/apis/provisioning/jobs/sync/worker.go index dfb376ca470..05e6340b193 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker.go @@ -110,25 +110,35 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo } syncStatus := job.Status.ToSyncStatus(job.Name) - // Preserve last ref as we use replace operation + // Preserve last ref lastRef := repo.Config().Status.Sync.LastRef syncStatus.LastRef = lastRef - if syncStatus.State == "" { - syncStatus.State = provisioning.JobStateWorking - } + // Ensure the sync state is set to 'working' if not already set or still pending. + // FIXME: This should not be needed as the progress recorder should have set it to 'working' by now. + syncStatus.State = provisioning.JobStateWorking - // Update sync status at start using JSON patch + // Update sync status at start using granular JSON patch operations + // Only patch fields that are actually being set to avoid overwriting with zero values patchOperations := []map[string]interface{}{ { "op": "replace", - "path": "/status/sync", - "value": syncStatus, + "path": "/status/sync/state", + "value": syncStatus.State, + }, + { + "op": "replace", + "path": "/status/sync/job", + "value": syncStatus.JobID, + }, + { + "op": "replace", + "path": "/status/sync/started", + "value": syncStatus.Started, }, } progress.SetMessage(ctx, "update sync status at start") - statusCtx, statusSpan := r.tracer.Start(ctx, "provisioning.sync.update_start_status") if err := r.patchStatus(statusCtx, cfg, patchOperations...); err != nil { statusSpan.End() @@ -174,14 +184,13 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo } syncSpan.End() - // Create sync status and set hash if successful - if syncStatus.State == provisioning.JobStateSuccess { + if syncStatus.State != provisioning.JobStateError { syncStatus.LastRef = currentRef } else { + // Preserve the original lastRef on error syncStatus.LastRef = lastRef } - // Update final status using JSON patch progress.SetMessage(ctx, "update status and stats") patchOperations = []map[string]interface{}{ { diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker_test.go b/pkg/registry/apis/provisioning/jobs/sync/worker_test.go index bf0b60bea01..a5013539957 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker_test.go @@ -115,17 +115,18 @@ func TestSyncWorker_Process(t *testing.T) { rw.MockRepository.On("Config").Return(repoConfig) pr.On("SetMessage", mock.Anything, "update sync status at start").Return() - rpf.On("Execute", mock.Anything, repoConfig, mock.MatchedBy(func(patch map[string]interface{}) bool { - if patch["op"] != "replace" || patch["path"] != "/status/sync" { - return false - } - - if patch["value"].(provisioning.SyncStatus).LastRef != "existing-ref" || patch["value"].(provisioning.SyncStatus).JobID != "test-job" { - return false - } - - return true - })).Return(errors.New("failed to patch status")) + // Expect granular patches for state, job, and started fields + rpf.On("Execute", mock.Anything, repoConfig, + mock.MatchedBy(func(patch map[string]interface{}) bool { + return patch["op"] == "replace" && patch["path"] == "/status/sync/state" + }), + mock.MatchedBy(func(patch map[string]interface{}) bool { + return patch["op"] == "replace" && patch["path"] == "/status/sync/job" + }), + mock.MatchedBy(func(patch map[string]interface{}) bool { + return patch["op"] == "replace" && patch["path"] == "/status/sync/started" + }), + ).Return(errors.New("failed to patch status")) }, expectedError: "update repo with job status at start: failed to patch status", }, @@ -151,9 +152,9 @@ func TestSyncWorker_Process(t *testing.T) { // Storage is migrated ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // Initial status update succeeds + // Initial status update succeeds - expect granular patches pr.On("SetMessage", mock.Anything, "update sync status at start").Return() - rpf.On("Execute", mock.Anything, repoConfig, mock.Anything).Return(nil).Once() + rpf.On("Execute", mock.Anything, repoConfig, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() // Repository resources creation fails rrf.On("Client", mock.Anything, mock.Anything).Return(nil, errors.New("failed to create repository resources client")) @@ -188,9 +189,9 @@ func TestSyncWorker_Process(t *testing.T) { // Storage is migrated ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // Initial status update succeeds + // Initial status update succeeds - expect granular patches pr.On("SetMessage", mock.Anything, "update sync status at start").Return() - rpf.On("Execute", mock.Anything, repoConfig, mock.Anything).Return(nil).Once() + rpf.On("Execute", mock.Anything, repoConfig, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() // Repository resources creation succeeds rrf.On("Client", mock.Anything, mock.Anything).Return(&resources.MockRepositoryResources{}, nil) @@ -224,9 +225,9 @@ func TestSyncWorker_Process(t *testing.T) { // Storage is migrated ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // Initial status update + // Initial status update - expect granular patches pr.On("SetMessage", mock.Anything, "update sync status at start").Return() - rpf.On("Execute", mock.Anything, repoConfig, mock.Anything).Return(nil) + rpf.On("Execute", mock.Anything, repoConfig, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() // Setup resources and clients mockRepoResources := resources.NewMockRepositoryResources(t) @@ -254,7 +255,7 @@ func TestSyncWorker_Process(t *testing.T) { } syncStatus := patch["value"].(provisioning.SyncStatus) return syncStatus.LastRef == "new-ref" && syncStatus.State == provisioning.JobStateSuccess - })).Return(nil) + })).Return(nil).Once() }, expectedError: "", }, @@ -277,9 +278,9 @@ func TestSyncWorker_Process(t *testing.T) { // Storage is migrated ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // Initial status update + // Initial status update - expect granular patches pr.On("SetMessage", mock.Anything, "update sync status at start").Return() - rpf.On("Execute", mock.Anything, repoConfig, mock.Anything).Return(nil) + rpf.On("Execute", mock.Anything, repoConfig, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() // Setup resources and clients mockRepoResources := resources.NewMockRepositoryResources(t) @@ -308,7 +309,7 @@ func TestSyncWorker_Process(t *testing.T) { patch["path"] == "/status/sync" && syncStatus.LastRef == "existing-ref" && // LastRef should not change on failure syncStatus.State == provisioning.JobStateError - })).Return(nil) + })).Return(nil).Once() }, expectedError: "sync operation failed", }, @@ -334,7 +335,9 @@ func TestSyncWorker_Process(t *testing.T) { pr.On("SetMessage", mock.Anything, mock.Anything).Return() pr.On("StrictMaxErrors", 20).Return() pr.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{State: provisioning.JobStateSuccess}) - rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything).Return(nil) + // Initial patch with granular updates, final patch with full sync status + rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() s.On("Sync", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("new-ref", nil) }, expectedError: "", @@ -355,10 +358,13 @@ func TestSyncWorker_Process(t *testing.T) { mockRepoResources.On("Stats", mock.Anything).Return(nil, nil) rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil) - // Verify only sync status is patched + // Initial patch with granular updates + rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + + // Verify only sync status is patched for final update rpf.On("Execute", mock.Anything, mock.Anything, mock.MatchedBy(func(patch map[string]interface{}) bool { return patch["path"] == "/status/sync" - })).Return(nil) + })).Return(nil).Once() // Simple mocks for other calls mockClients := resources.NewMockResourceClients(t) @@ -381,7 +387,8 @@ func TestSyncWorker_Process(t *testing.T) { } rw.MockRepository.On("Config").Return(repoConfig) ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + // Initial patch with granular updates + rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() mockRepoResources := resources.NewMockRepositoryResources(t) stats := &provisioning.ResourceStats{ @@ -468,10 +475,13 @@ func TestSyncWorker_Process(t *testing.T) { mockRepoResources.On("Stats", mock.Anything).Return(stats, nil) rrf.On("Client", mock.Anything, mock.Anything).Return(mockRepoResources, nil) + // Initial patch with granular updates + rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + // Verify only sync status is patched (multiple stats should be ignored) rpf.On("Execute", mock.Anything, mock.Anything, mock.MatchedBy(func(patch map[string]interface{}) bool { return patch["path"] == "/status/sync" - })).Return(nil) + })).Return(nil).Once() // Simple mocks for other calls mockClients := resources.NewMockResourceClients(t) @@ -495,8 +505,8 @@ func TestSyncWorker_Process(t *testing.T) { rw.MockRepository.On("Config").Return(repoConfig) ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - // Initial status patch succeeds - rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + // Initial status patch succeeds - expect granular patches + rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() // Setup resources and clients mockRepoResources := resources.NewMockRepositoryResources(t) diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 8797d940408..13104e3e2f5 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -35,6 +35,7 @@ import ( clientset "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned" client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" informers "github.com/grafana/grafana/apps/provisioning/pkg/generated/informers/externalversions" + jobsvalidation "github.com/grafana/grafana/apps/provisioning/pkg/jobs" "github.com/grafana/grafana/apps/provisioning/pkg/loki" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -87,7 +88,8 @@ type APIBuilder struct { // onlyApiServer used to disable starting controllers for the standalone API server. // HACK:This will be removed once we have proper wire providers for the controllers. // TODO: Set this up in the standalone API server - onlyApiServer bool + onlyApiServer bool + useExclusivelyAccessCheckerForAuthz bool allowedTargets []provisioning.SyncTargetType allowImageRendering bool @@ -147,6 +149,7 @@ func NewAPIBuilder( minSyncInterval time.Duration, registry prometheus.Registerer, newStandaloneClientFactoryFunc func(loopbackConfigProvider apiserver.RestConfigProvider) resources.ClientFactory, // optional, only used for standalone apiserver + useExclusivelyAccessCheckerForAuthz bool, ) *APIBuilder { var clients resources.ClientFactory if newStandaloneClientFactoryFunc != nil { @@ -158,26 +161,27 @@ func NewAPIBuilder( resourceLister := resources.NewResourceListerForMigrations(unified, legacyMigrator, storageStatus) b := &APIBuilder{ - onlyApiServer: onlyApiServer, - tracer: tracer, - usageStats: usageStats, - features: features, - repoFactory: repoFactory, - clients: clients, - parsers: parsers, - repositoryResources: resources.NewRepositoryResourcesFactory(parsers, clients, resourceLister), - resourceLister: resourceLister, - legacyMigrator: legacyMigrator, - storageStatus: storageStatus, - unified: unified, - access: access, - jobHistoryConfig: jobHistoryConfig, - extraWorkers: extraWorkers, - restConfigGetter: restConfigGetter, - allowedTargets: allowedTargets, - allowImageRendering: allowImageRendering, - registry: registry, - validator: repository.NewValidator(minSyncInterval, allowedTargets, allowImageRendering), + onlyApiServer: onlyApiServer, + tracer: tracer, + usageStats: usageStats, + features: features, + repoFactory: repoFactory, + clients: clients, + parsers: parsers, + repositoryResources: resources.NewRepositoryResourcesFactory(parsers, clients, resourceLister), + resourceLister: resourceLister, + legacyMigrator: legacyMigrator, + storageStatus: storageStatus, + unified: unified, + access: access, + jobHistoryConfig: jobHistoryConfig, + extraWorkers: extraWorkers, + restConfigGetter: restConfigGetter, + allowedTargets: allowedTargets, + allowImageRendering: allowImageRendering, + registry: registry, + validator: repository.NewValidator(minSyncInterval, allowedTargets, allowImageRendering), + useExclusivelyAccessCheckerForAuthz: useExclusivelyAccessCheckerForAuthz, } for _, builder := range extraBuilders { @@ -267,6 +271,7 @@ func RegisterAPIService( cfg.ProvisioningMinSyncInterval, reg, nil, + false, // TODO: first, test this on the MT side before we enable it by default in ST as well ) apiregistration.RegisterAPI(builder) return builder, nil @@ -283,7 +288,9 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { } info, ok := authlib.AuthInfoFrom(ctx) - if ok && authlib.IsIdentityType(info.GetIdentityType(), authlib.TypeAccessPolicy) { + // when running as standalone API server, the identity type may not always match TypeAccessPolicy + // so we allow it to use the access checker if there is any auth info available + if ok && (authlib.IsIdentityType(info.GetIdentityType(), authlib.TypeAccessPolicy) || b.useExclusivelyAccessCheckerForAuthz) { res, err := b.access.Check(ctx, info, authlib.CheckRequest{ Verb: a.GetVerb(), Group: a.GetAPIGroup(), @@ -291,6 +298,7 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { Name: a.GetName(), Namespace: a.GetNamespace(), Subresource: a.GetSubresource(), + Path: a.GetPath(), }, "") if err != nil { return authorizer.DecisionDeny, "failed to perform authorization", err @@ -569,10 +577,10 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm return nil } - // FIXME: Do nothing for Jobs for now - _, ok = obj.(*provisioning.Job) + // Validate Jobs + job, ok := obj.(*provisioning.Job) if ok { - return nil + return jobsvalidation.ValidateJob(job) } repo, err := b.asRepository(ctx, obj, a.GetOldObject()) @@ -696,7 +704,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH metrics, ) - syncer := sync.NewSyncer(sync.Compare, sync.FullSync, sync.IncrementalSync, b.tracer, 10) + syncer := sync.NewSyncer(sync.Compare, sync.FullSync, sync.IncrementalSync, b.tracer, 10, metrics) syncWorker := sync.NewSyncWorker( b.clients, b.repositoryResources, @@ -798,6 +806,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.statusPatcher, b.registry, b.tracer, + 10, ) if err != nil { return err diff --git a/pkg/registry/apis/provisioning/resources/dualwriter.go b/pkg/registry/apis/provisioning/resources/dualwriter.go index 052771af700..62f4ffd3b98 100644 --- a/pkg/registry/apis/provisioning/resources/dualwriter.go +++ b/pkg/registry/apis/provisioning/resources/dualwriter.go @@ -100,7 +100,7 @@ func (r *DualReadWriter) Delete(ctx context.Context, opts DualWriteOptions) (*Pa } // HACK: manual set to the provided branch so that the parser can possible read the file - if r.shouldUpdateGrafanaDB(opts, nil) { + if !r.shouldUpdateGrafanaDB(opts, nil) { file.Ref = opts.Ref } diff --git a/pkg/registry/apis/provisioning/webhooks/webhook.go b/pkg/registry/apis/provisioning/webhooks/webhook.go index 4e5bfffb28a..859437f1dda 100644 --- a/pkg/registry/apis/provisioning/webhooks/webhook.go +++ b/pkg/registry/apis/provisioning/webhooks/webhook.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "go.opentelemetry.io/otel/attribute" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/authorization/authorizer" @@ -17,6 +18,7 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" provisioningapis "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest" "github.com/prometheus/client_golang/prometheus" @@ -122,8 +124,16 @@ func (s *webhookConnector) Connect(ctx context.Context, name string, opts runtim } return provisioningapis.WithTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger := logging.FromContext(r.Context()).With("logger", "webhook-connector", "repo", name) - ctx := logging.Context(r.Context(), logger) + ctx, span := tracing.Start(r.Context(), "provisioning.webhook.handle") + defer span.End() + + span.SetAttributes( + attribute.String("repository", name), + attribute.String("namespace", namespace), + ) + + logger := logging.FromContext(ctx).With("logger", "webhook-connector", "repo", name) + ctx = logging.Context(ctx, logger) if !s.webhooksEnabled { responder.Error(errors.NewBadRequest("webhooks are not enabled")) return @@ -140,12 +150,15 @@ func (s *webhookConnector) Connect(ctx context.Context, name string, opts runtim rsp, err := hooks.Webhook(ctx, r) if err != nil { + span.RecordError(err) responder.Error(err) return } if rsp == nil { - responder.Error(fmt.Errorf("expecting a response")) + err := fmt.Errorf("expecting a response") + span.RecordError(err) + responder.Error(err) return } @@ -162,12 +175,17 @@ func (s *webhookConnector) Connect(ctx context.Context, name string, opts runtim if rsp.Job != nil { rsp.Job.Repository = name actionTaken = string(rsp.Job.Action) + span.SetAttributes(attribute.String("job.action", actionTaken)) + job, err := s.core.GetJobQueue().Insert(ctx, namespace, *rsp.Job) if err != nil { + span.RecordError(err) logger.Error("failed to insert job", "error", err) responder.Error(err) return } + span.SetAttributes(attribute.String("job.name", job.Name)) + logger.Info("webhook job created", "job", job.Name, "action", actionTaken) responder.Object(rsp.Code, job) return } diff --git a/pkg/registry/apis/query/client/instance_provider.go b/pkg/registry/apis/query/client/instance_provider.go index e48e2e9a084..9a8e6138b65 100644 --- a/pkg/registry/apis/query/client/instance_provider.go +++ b/pkg/registry/apis/query/client/instance_provider.go @@ -52,6 +52,10 @@ func (s *singleTenantInstanceProvider) GetInstance(_ context.Context, logger log }, nil } +func (s *singleTenantInstanceProvider) GetMode() string { + return "st" +} + func (s *singleTenantInstance) GetSettings() clientapi.InstanceConfigurationSettings { return s.instanceConf } diff --git a/pkg/registry/apis/query/clientapi/clientapi.go b/pkg/registry/apis/query/clientapi/clientapi.go index 5d44045f77f..0248ebab8ac 100644 --- a/pkg/registry/apis/query/clientapi/clientapi.go +++ b/pkg/registry/apis/query/clientapi/clientapi.go @@ -39,4 +39,5 @@ type Instance interface { type InstanceProvider interface { GetInstance(ctx context.Context, logger log.Logger, headers map[string]string) (Instance, error) + GetMode() string } diff --git a/pkg/registry/apis/query/header_utils.go b/pkg/registry/apis/query/header_utils.go index efbbbecd18d..1284f0c2648 100644 --- a/pkg/registry/apis/query/header_utils.go +++ b/pkg/registry/apis/query/header_utils.go @@ -40,8 +40,6 @@ var expectedHeaders = map[string]string{ strings.ToLower(queryService.HeaderPanelPluginId): queryService.HeaderPanelPluginId, strings.ToLower(queryService.HeaderDashboardTitle): queryService.HeaderDashboardTitle, strings.ToLower(queryService.HeaderPanelTitle): queryService.HeaderPanelTitle, - strings.ToLower("X-Real-IP"): "X-Real-IP", - strings.ToLower("X-Forwarded-For"): "X-Forwarded-For", } func ExtractKnownHeaders(header http.Header) map[string]string { diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index f63cb850bc7..987251bcdb0 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -122,6 +122,8 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O b := r.builder return http.HandlerFunc(func(w http.ResponseWriter, httpreq *http.Request) { + w.Header().Set("X-Ds-Querier", b.instanceProvider.GetMode()) + ctx, span := b.tracer.Start(httpreq.Context(), "QueryService.Query") defer span.End() ctx = request.WithNamespace(ctx, request.NamespaceValue(connectCtx)) @@ -241,25 +243,40 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O }), nil } -func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuilder, httpreq *http.Request, responder responderWrapper, connectLogger log.Logger) (*backend.QueryDataResponse, error) { - var jsonQueries = make([]*simplejson.Json, 0, len(raw.Queries)) - for _, query := range raw.Queries { - dsRef, err := getValidDataSourceRef(ctx, query.Datasource, query.DatasourceID, b.legacyDatasourceLookup) - if err != nil { - connectLogger.Error("error getting valid datasource ref", err) - } - if dsRef != nil { - query.Datasource = dsRef +type preparedQuery struct { + mReq dtos.MetricRequest + cache datasources.CacheService + headers map[string]string + logger log.Logger + builder dsquerierclient.QSDatasourceClientBuilder + exprSvc *expr.Service + reportMetrics func() +} + +func prepareQuery( + ctx context.Context, + raw query.QueryDataRequest, + b QueryAPIBuilder, + httpreq *http.Request, + connectLogger log.Logger, +) (*preparedQuery, error) { + // Normalize DS refs and build []*simplejson.Json + jsonQueries := make([]*simplejson.Json, 0, len(raw.Queries)) + for _, q := range raw.Queries { + if dsRef, derr := getValidDataSourceRef(ctx, q.Datasource, q.DatasourceID, b.legacyDatasourceLookup); derr != nil { + connectLogger.Error("error getting valid datasource ref", "err", derr) + } else if dsRef != nil { + q.Datasource = dsRef } - jsonBytes, err := json.Marshal(query) + jsonBytes, err := json.Marshal(q) if err != nil { - connectLogger.Error("error marshalling", err) + connectLogger.Error("error marshalling query", "err", err) } - sjQuery, _ := simplejson.NewJson(jsonBytes) + sjQuery, err := simplejson.NewJson(jsonBytes) if err != nil { - connectLogger.Error("error unmarshalling", err) + connectLogger.Error("error creating simplejson for query", "err", err) } jsonQueries = append(jsonQueries, sjQuery) @@ -274,13 +291,11 @@ func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuil cache := &MyCacheService{ legacy: b.legacyDatasourceLookup, } - headers := ExtractKnownHeaders(httpreq.Header) instance, err := b.instanceProvider.GetInstance(ctx, connectLogger, headers) if err != nil { connectLogger.Error("failed to get instance configuration settings", "err", err) - responder.Error(err) return nil, err } @@ -288,12 +303,14 @@ func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuil dsQuerierLoggerWithSlug := instance.GetLogger() + // Datasource client qsDsClientBuilder qsDsClientBuilder := dsquerierclient.NewQsDatasourceClientBuilderWithInstance( instance, ctx, dsQuerierLoggerWithSlug, ) + // Expressions service exprService := expr.ProvideService( &setting.Cfg{ ExpressionsEnabled: instanceConfig.ExpressionsEnabled, @@ -310,17 +327,37 @@ func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuil qsDsClientBuilder, ) - qdr, err := service.QueryData(ctx, dsQuerierLoggerWithSlug, cache, exprService, mReq, qsDsClientBuilder, headers) + return &preparedQuery{ + mReq: mReq, + cache: cache, + headers: headers, + logger: dsQuerierLoggerWithSlug, + builder: qsDsClientBuilder, + exprSvc: exprService, + reportMetrics: func() { instance.ReportMetrics() }, + }, nil +} - // tell the `instance` structure that it can now report - // metrics that are only reported once during a request - instance.ReportMetrics() +func handlePreparedQuery(ctx context.Context, pq *preparedQuery) (*backend.QueryDataResponse, error) { + resp, err := service.QueryData(ctx, pq.logger, pq.cache, pq.exprSvc, pq.mReq, pq.builder, pq.headers) + pq.reportMetrics() + return resp, err +} +func handleQuery( + ctx context.Context, + raw query.QueryDataRequest, + b QueryAPIBuilder, + httpreq *http.Request, + responder responderWrapper, + connectLogger log.Logger, +) (*backend.QueryDataResponse, error) { + pq, err := prepareQuery(ctx, raw, b, httpreq, connectLogger) if err != nil { - return qdr, err + responder.Error(err) + return nil, err } - - return qdr, nil + return handlePreparedQuery(ctx, pq) } type responderWrapper struct { diff --git a/pkg/registry/apis/query/query_test.go b/pkg/registry/apis/query/query_test.go index f94cc92f317..b23c78d55bc 100644 --- a/pkg/registry/apis/query/query_test.go +++ b/pkg/registry/apis/query/query_test.go @@ -54,6 +54,10 @@ func (mu mockUser) GetOrgID() int64 { return -1 } +func (mu mockUser) GetNamespace() string { + return "ns" +} + func TestQueryAPI(t *testing.T) { testCases := []struct { name string @@ -270,6 +274,10 @@ func (m mockClient) GetInstance(ctx context.Context, logger log.Logger, headers return mclient, nil } +func (m mockClient) GetMode() string { + return "testing" +} + func (m mockClient) ReportMetrics() { } diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index 109de13276c..221b589b511 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -161,6 +161,7 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { &query.QueryDataResponse{}, &query.QueryTypeDefinition{}, &query.QueryTypeDefinitionList{}, + &query.SQLSchemas{}, ) } @@ -201,6 +202,8 @@ func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIG // The query endpoint -- NOTE, this uses a rewrite hack to allow requests without a name parameter storage["query"] = newQueryREST(b) + storage["sqlschemas"] = newSQLSchemasREST(b) + // Register the expressions query schemas err := queryschema.RegisterQueryTypes(b.queryTypes, storage) diff --git a/pkg/registry/apis/query/sql_schema.go b/pkg/registry/apis/query/sql_schema.go new file mode 100644 index 00000000000..10ec3dc2526 --- /dev/null +++ b/pkg/registry/apis/query/sql_schema.go @@ -0,0 +1,170 @@ +package query + +import ( + "context" + "net/http" + "strconv" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/expr" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + errorsK8s "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + service "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/web" +) + +type sqlSchemaREST struct { + logger log.Logger + builder *QueryAPIBuilder +} + +var ( + _ rest.Storage = (*sqlSchemaREST)(nil) + _ rest.SingularNameProvider = (*sqlSchemaREST)(nil) + _ rest.Connecter = (*sqlSchemaREST)(nil) + _ rest.Scoper = (*sqlSchemaREST)(nil) + _ rest.StorageMetadata = (*sqlSchemaREST)(nil) +) + +func newSQLSchemasREST(builder *QueryAPIBuilder) *sqlSchemaREST { + return &sqlSchemaREST{ + logger: log.New("query.sqlschemas"), + builder: builder, + } +} + +func (r *sqlSchemaREST) New() runtime.Object { + // This is added as the "ResponseType" regardless what ProducesObject() says :) + return &query.SQLSchemas{} +} + +func (r *sqlSchemaREST) Destroy() {} + +func (r *sqlSchemaREST) NamespaceScoped() bool { + return true +} + +func (r *sqlSchemaREST) GetSingularName() string { + return "SQLSchema" // Used for the +} + +func (r *sqlSchemaREST) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} // and parquet! +} + +func (r *sqlSchemaREST) ProducesObject(verb string) interface{} { + return &query.SQLSchemas{} +} + +func (r *sqlSchemaREST) ConnectMethods() []string { + return []string{"POST"} +} + +func (r *sqlSchemaREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" // true means you can use the trailing path as a variable +} + +// called by mt query service and also when queryServiceFromUI is enabled, can be both mt and st +func (r *sqlSchemaREST) Connect(connectCtx context.Context, name string, _ runtime.Object, incomingResponder rest.Responder) (http.Handler, error) { + // See: /pkg/services/apiserver/builder/helper.go#L34 + // The name is set with a rewriter hack + if name != "name" { + r.logger.Debug("Connect name is not name") + return nil, errorsK8s.NewNotFound(schema.GroupResource{}, name) + } + b := r.builder + + return http.HandlerFunc(func(w http.ResponseWriter, httpreq *http.Request) { + ctx, span := b.tracer.Start(httpreq.Context(), "QueryService.GetSQLSchemas") + defer span.End() + ctx = request.WithNamespace(ctx, request.NamespaceValue(connectCtx)) + traceId := span.SpanContext().TraceID() + connectLogger := b.log.New("traceId", traceId.String(), "rule_uid", httpreq.Header.Get("X-Rule-Uid")) + responder := newResponderWrapper(incomingResponder, + func(statusCode *int, obj runtime.Object) { + if *statusCode/100 == 4 { + span.SetStatus(codes.Error, strconv.Itoa(*statusCode)) + } + + if *statusCode >= 500 { + o, ok := obj.(*query.QueryDataResponse) + if ok && o.Responses != nil { + for refId, response := range o.Responses { + if response.ErrorSource == backend.ErrorSourceDownstream { + *statusCode = http.StatusBadRequest //force this to be a 400 since it's downstream + span.SetStatus(codes.Error, strconv.Itoa(*statusCode)) + span.SetAttributes(attribute.String("error.source", "downstream")) + break + } else if response.Error != nil { + connectLogger.Debug("500 error without downstream error source", "error", response.Error, "errorSource", response.ErrorSource, "refId", refId) + span.SetStatus(codes.Error, "500 error without downstream error source") + } else { + span.SetStatus(codes.Error, "500 error without downstream error source and no Error message") + span.SetAttributes(attribute.String("error.ref_id", refId)) + } + } + } + } + }, + + func(err error) { + connectLogger.Error("error caught in handler", "err", err) + span.SetStatus(codes.Error, "query error") + + if err == nil { + return + } + + span.RecordError(err) + }) + + raw := &query.QueryDataRequest{} + err := web.Bind(httpreq, raw) + if err != nil { + connectLogger.Error("Hit unexpected error when reading query", "err", err) + err = errorsK8s.NewBadRequest("error reading query") + responder.Error(err) + return + } + + qdr, err := handleSQLSchemaQuery(ctx, *raw, *b, httpreq, *responder, connectLogger) + if err != nil { + responder.Error(err) + return + } + + responder.Object(200, &query.SQLSchemas{ + SQLSchemas: qdr, + }) + }), nil +} + +func handlePreparedSQLSchema(ctx context.Context, pq *preparedQuery) (expr.SQLSchemas, error) { + resp, err := service.GetSQLSchemas(ctx, pq.logger, pq.cache, pq.exprSvc, pq.mReq, pq.builder, pq.headers) + pq.reportMetrics() + return resp, err +} + +func handleSQLSchemaQuery( + ctx context.Context, + raw query.QueryDataRequest, + b QueryAPIBuilder, + httpreq *http.Request, + responder responderWrapper, + connectLogger log.Logger, +) (expr.SQLSchemas, error) { + pq, err := prepareQuery(ctx, raw, b, httpreq, connectLogger) + if err != nil { + responder.Error(err) + return nil, err + } + return handlePreparedSQLSchema(ctx, pq) +} diff --git a/pkg/registry/apis/secret/contracts/encryption.go b/pkg/registry/apis/secret/contracts/encryption.go index f24fe73a544..73a4952cac2 100644 --- a/pkg/registry/apis/secret/contracts/encryption.go +++ b/pkg/registry/apis/secret/contracts/encryption.go @@ -1,6 +1,10 @@ package contracts -import "context" +import ( + "context" + + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) // EncryptionManager is an envelope encryption service in charge of encrypting/decrypting secrets. type EncryptionManager interface { @@ -8,17 +12,23 @@ type EncryptionManager interface { // For those specific use cases where the encryption operation cannot be moved outside // the database transaction, look at database-specific methods present at the specific // implementation present at manager.EncryptionService. - Encrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) - Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) + Encrypt(ctx context.Context, namespace xkube.Namespace, payload []byte) (EncryptedPayload, error) + Decrypt(ctx context.Context, namespace xkube.Namespace, payload EncryptedPayload) ([]byte, error) +} + +type EncryptedPayload struct { + DataKeyID string + EncryptedData []byte } type EncryptedValue struct { - Namespace string - Name string - Version int64 - EncryptedData []byte - Created int64 - Updated int64 + EncryptedPayload + + Namespace string + Name string + Version int64 + Created int64 + Updated int64 } // ListOpts defines pagination options for listing encrypted values. @@ -28,10 +38,10 @@ type ListOpts struct { } type EncryptedValueStorage interface { - Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (*EncryptedValue, error) - Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error - Get(ctx context.Context, namespace, name string, version int64) (*EncryptedValue, error) - Delete(ctx context.Context, namespace, name string, version int64) error + Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData EncryptedPayload) (*EncryptedValue, error) + Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData EncryptedPayload) error + Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*EncryptedValue, error) + Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error } type GlobalEncryptedValueStorage interface { @@ -39,6 +49,10 @@ type GlobalEncryptedValueStorage interface { CountAll(ctx context.Context, untilTime *int64) (int64, error) } +type EncryptedValueMigrationExecutor interface { + Execute(ctx context.Context) (int, error) +} + type ConsolidationService interface { Consolidate(ctx context.Context) error } diff --git a/pkg/registry/apis/secret/contracts/keeper.go b/pkg/registry/apis/secret/contracts/keeper.go index 5558a4db3b5..a204bb5b1a7 100644 --- a/pkg/registry/apis/secret/contracts/keeper.go +++ b/pkg/registry/apis/secret/contracts/keeper.go @@ -96,10 +96,10 @@ func (s ExternalID) String() string { // Keeper is the interface for secret keepers. type Keeper interface { - Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error) - Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error - Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) - Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error + Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error) + Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error + Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) + Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error } // Service is the interface for secret keeper services. diff --git a/pkg/registry/apis/secret/encryption/manager/manager.go b/pkg/registry/apis/secret/encryption/manager/manager.go index f48b350c1bd..024c8aca05a 100644 --- a/pkg/registry/apis/secret/encryption/manager/manager.go +++ b/pkg/registry/apis/secret/encryption/manager/manager.go @@ -1,10 +1,8 @@ package manager import ( - "bytes" "context" "crypto/rand" - "encoding/base64" "errors" "fmt" "strconv" @@ -20,13 +18,10 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/util" ) -const ( - keyIdDelimiter = '#' -) - type EncryptionManager struct { tracer trace.Tracer store contracts.DataKeyStorage @@ -99,12 +94,9 @@ func (s *EncryptionManager) registerUsageMetrics() { }) } -// TODO: Why do we need to use a global variable for this? -var b64 = base64.RawStdEncoding - -func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) { +func (s *EncryptionManager) Encrypt(ctx context.Context, namespace xkube.Namespace, payload []byte) (contracts.EncryptedPayload, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.Encrypt", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -128,34 +120,30 @@ func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, paylo id, dataKey, err = s.currentDataKey(ctx, namespace, label) if err != nil { s.log.Error("Failed to get current data key", "error", err, "label", label) - return nil, err + return contracts.EncryptedPayload{}, err } var encrypted []byte encrypted, err = s.cipher.Encrypt(ctx, payload, string(dataKey)) if err != nil { s.log.Error("Failed to encrypt secret", "error", err) - return nil, err + return contracts.EncryptedPayload{}, err } - prefix := make([]byte, b64.EncodedLen(len(id))+2) - b64.Encode(prefix[1:], []byte(id)) - prefix[0] = keyIdDelimiter - prefix[len(prefix)-1] = keyIdDelimiter + encryptedPayload := contracts.EncryptedPayload{ + DataKeyID: id, + EncryptedData: encrypted, + } - blob := make([]byte, len(prefix)+len(encrypted)) - copy(blob, prefix) - copy(blob[len(prefix):], encrypted) - - return blob, nil + return encryptedPayload, nil } // currentDataKey looks up for current data key in cache or database by name, and decrypts it. // If there's no current data key in cache nor in database it generates a new random data key, // and stores it into both the in-memory cache and database (encrypted by the encryption provider). -func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace string, label string) (string, []byte, error) { +func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace xkube.Namespace, label string) (string, []byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.CurrentDataKey", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("label", label), )) defer span.End() @@ -166,14 +154,14 @@ func (s *EncryptionManager) currentDataKey(ctx context.Context, namespace string defer s.mtx.Unlock() // We try to fetch the data key, either from cache or database - id, dataKey, err := s.dataKeyByLabel(ctx, namespace, label) + id, dataKey, err := s.dataKeyByLabel(ctx, namespace.String(), label) if err != nil { return "", nil, err } // If no existing data key was found, create a new one if dataKey == nil { - id, dataKey, err = s.newDataKey(ctx, namespace, label) + id, dataKey, err = s.newDataKey(ctx, namespace.String(), label) if err != nil { return "", nil, err } @@ -264,9 +252,9 @@ func newRandomDataKey() ([]byte, error) { return rawDataKey, nil } -func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, payload []byte) ([]byte, error) { +func (s *EncryptionManager) Decrypt(ctx context.Context, namespace xkube.Namespace, payload contracts.EncryptedPayload) ([]byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.Decrypt", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -285,50 +273,28 @@ func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, paylo } }() - if len(payload) == 0 { + if len(payload.EncryptedData) == 0 { err = fmt.Errorf("unable to decrypt empty payload") return nil, err } - payload = payload[1:] - endOfKey := bytes.Index(payload, []byte{keyIdDelimiter}) - if endOfKey == -1 { - err = fmt.Errorf("could not find valid key id in encrypted payload") - return nil, err - } - b64Key := payload[:endOfKey] - payload = payload[endOfKey+1:] - keyId := make([]byte, b64.DecodedLen(len(b64Key))) - _, err = b64.Decode(keyId, b64Key) - if err != nil { + if payload.DataKeyID == "" { + err = fmt.Errorf("unable to decrypt empty data key id") return nil, err } - dataKey, err := s.dataKeyById(ctx, namespace, string(keyId)) + dataKey, err := s.dataKeyById(ctx, namespace.String(), payload.DataKeyID) if err != nil { - s.log.FromContext(ctx).Error("Failed to lookup data key by id", "id", string(keyId), "error", err) + s.log.FromContext(ctx).Error("Failed to lookup data key by id", "id", payload.DataKeyID, "error", err) return nil, err } var decrypted []byte - decrypted, err = s.cipher.Decrypt(ctx, payload, string(dataKey)) + decrypted, err = s.cipher.Decrypt(ctx, payload.EncryptedData, string(dataKey)) return decrypted, err } -func (s *EncryptionManager) GetDecryptedValue(ctx context.Context, namespace string, sjd map[string][]byte, key, fallback string) string { - if value, ok := sjd[key]; ok { - decryptedData, err := s.Decrypt(ctx, namespace, value) - if err != nil { - return fallback - } - - return string(decryptedData) - } - - return fallback -} - // dataKeyById looks up for data key in the database and returns it decrypted. func (s *EncryptionManager) dataKeyById(ctx context.Context, namespace, id string) ([]byte, error) { ctx, span := s.tracer.Start(ctx, "EnvelopeEncryptionManager.GetDataKey", trace.WithAttributes( diff --git a/pkg/registry/apis/secret/encryption/manager/manager_test.go b/pkg/registry/apis/secret/encryption/manager/manager_test.go index a6c1c06bcfa..dc63fb461c9 100644 --- a/pkg/registry/apis/secret/encryption/manager/manager_test.go +++ b/pkg/registry/apis/secret/encryption/manager/manager_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -34,7 +35,7 @@ func TestMain(m *testing.M) { func TestEncryptionService_EnvelopeEncryption(t *testing.T) { svc := setupTestService(t) ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") t.Run("encrypting should create DEK", func(t *testing.T) { plaintext := []byte("very secret string") @@ -46,7 +47,7 @@ func TestEncryptionService_EnvelopeEncryption(t *testing.T) { require.NoError(t, err) assert.Equal(t, plaintext, decrypted) - keys, err := svc.store.ListDataKeys(ctx, namespace) + keys, err := svc.store.ListDataKeys(ctx, namespace.String()) require.NoError(t, err) assert.Equal(t, len(keys), 1) }) @@ -61,7 +62,7 @@ func TestEncryptionService_EnvelopeEncryption(t *testing.T) { require.NoError(t, err) assert.Equal(t, plaintext, decrypted) - keys, err := svc.store.ListDataKeys(ctx, namespace) + keys, err := svc.store.ListDataKeys(ctx, namespace.String()) require.NoError(t, err) assert.Equal(t, len(keys), 1) }) @@ -212,7 +213,7 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { } encryptionManager.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1") - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") encrypted, _ := encryptionManager.Encrypt(context.Background(), namespace, []byte{}) assert.True(t, fake.encryptCalled) assert.False(t, fake.decryptCalled) @@ -241,7 +242,7 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { func TestEncryptionService_SecretKeyVersionUpgrade(t *testing.T) { ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") // Generate random keys for testing oldKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars @@ -416,16 +417,30 @@ func (p *fakeProvider) Decrypt(_ context.Context, _ []byte) ([]byte, error) { func TestEncryptionService_Decrypt(t *testing.T) { ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") t.Run("empty payload should fail", func(t *testing.T) { svc := setupTestService(t) - _, err := svc.Decrypt(context.Background(), namespace, []byte("")) + _, err := svc.Decrypt(context.Background(), namespace, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte(""), + }) require.Error(t, err) assert.Equal(t, "unable to decrypt empty payload", err.Error()) }) + t.Run("empty data key id should fail", func(t *testing.T) { + svc := setupTestService(t) + _, err := svc.Decrypt(context.Background(), namespace, contracts.EncryptedPayload{ + DataKeyID: "", + EncryptedData: []byte("some payload"), + }) + require.Error(t, err) + + assert.Equal(t, "unable to decrypt empty data key id", err.Error()) + }) + t.Run("ee encrypted payload with ee enabled should work", func(t *testing.T) { svc := setupTestService(t) ciphertext, err := svc.Encrypt(ctx, namespace, []byte("grafana")) @@ -442,7 +457,7 @@ func TestIntegration_SecretsService(t *testing.T) { ctx := context.Background() someData := []byte(`some-data`) - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") tcs := map[string]func(*testing.T, db.DB, contracts.EncryptionManager){ "regular": func(t *testing.T, _ db.DB, svc contracts.EncryptionManager) { @@ -562,7 +577,7 @@ func TestIntegration_SecretsService(t *testing.T) { require.NoError(t, err) ctx := context.Background() - namespace := "test-namespace" + namespace := xkube.Namespace("test-namespace") // Here's what actually matters and varies on each test: look at the test case name. // diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker.go b/pkg/registry/apis/secret/garbagecollectionworker/worker.go index 4754f06c02d..e10bbed600e 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker.go @@ -104,7 +104,7 @@ func (w *Worker) Cleanup(ctx context.Context, sv *secretv1beta1.SecureValue) err } // Keeper deletion is idempotent - if err := keeper.Delete(ctx, keeperCfg, sv.Namespace, sv.Name, sv.Status.Version); err != nil { + if err := keeper.Delete(ctx, keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version); err != nil { return fmt.Errorf("deleting secure value from keeper: %w", err) } diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go index 344d2bcf605..23467d69b39 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go @@ -9,6 +9,7 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/mitchellh/copystructure" "github.com/stretchr/testify/require" @@ -58,7 +59,7 @@ func TestBasic(t *testing.T) { require.NoError(t, err) // Get the secret value once to make sure it's reachable - exposedValue, err := keeper.Expose(t.Context(), keeperCfg, sv.Namespace, sv.Name, sv.Status.Version) + exposedValue, err := keeper.Expose(t.Context(), keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version) require.NoError(t, err) require.NotEmpty(t, exposedValue.DangerouslyExposeAndConsumeValue()) @@ -78,7 +79,7 @@ func TestBasic(t *testing.T) { require.Empty(t, svs) // Try to get the secreet value again to make sure it's been deleted from the keeper - exposedValue, err = keeper.Expose(t.Context(), keeperCfg, sv.Namespace, sv.Name, sv.Status.Version) + exposedValue, err = keeper.Expose(t.Context(), keeperCfg, xkube.Namespace(sv.Namespace), sv.Name, sv.Status.Version) require.ErrorIs(t, err, encryption.ErrEncryptedValueNotFound) require.Empty(t, exposedValue) }) diff --git a/pkg/registry/apis/secret/register.go b/pkg/registry/apis/secret/register.go index 879741dc948..155bc20d5ff 100644 --- a/pkg/registry/apis/secret/register.go +++ b/pkg/registry/apis/secret/register.go @@ -25,7 +25,7 @@ func RegisterDependencies( } // We shouldn't need to create the DB in HG, as that will use the MT api server. - if cfg.StackID == "" || cfg.SecretsManagement.IsDeveloperMode { + if cfg.SecretsManagement.RunSecretsDBMigrations { // Some DBs that claim to be MySQL/Postgres-compatible might not support table locking. lockDatabase := cfg.Raw.Section("database").Key("migration_locking").MustBool(true) diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go index fc65573ac61..25241f5c15b 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go @@ -1,11 +1,15 @@ package secretkeeper import ( + "fmt" + "go.opentelemetry.io/otel/trace" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" + "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" ) @@ -20,11 +24,18 @@ func ProvideService( tracer trace.Tracer, store contracts.EncryptedValueStorage, encryptionManager contracts.EncryptionManager, + migrationExecutor contracts.EncryptedValueMigrationExecutor, reg prometheus.Registerer, + cfg *setting.Cfg, + _ *secret.DependencyRegisterer, // noop import so wire runs DB migrations before instantiating this service -- can be nil when manually instantiating ) (*OSSKeeperService, error) { + systemKeeper, err := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, store, migrationExecutor, reg, cfg) + if err != nil { + return nil, fmt.Errorf("failed to create system keeper: %w", err) + } + return &OSSKeeperService{ - // TODO: rename to system keeper or something like that - systemKeeper: sqlkeeper.NewSQLKeeper(tracer, encryptionManager, store, reg), + systemKeeper: systemKeeper, }, nil } diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go index 84baaf207b0..f60dcc91188 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go @@ -12,6 +12,7 @@ import ( osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -65,7 +66,8 @@ func setupTestService(t *testing.T, cfg *setting.Cfg) (*OSSKeeperService, error) require.NoError(t, err) // Initialize the keeper service - keeperService, err := ProvideService(tracer, encValueStore, encryptionManager, nil) + keeperService, err := ProvideService(tracer, encValueStore, encryptionManager, &testutils.NoopMigrationExecutor{}, nil, cfg, nil) + require.NoError(t, err) return keeperService, err } diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go index bfe3972f72b..294eff1a7af 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go @@ -5,9 +5,12 @@ import ( "fmt" "time" + "github.com/grafana/grafana-app-sdk/logging" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/metrics" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -26,20 +29,36 @@ func NewSQLKeeper( tracer trace.Tracer, encryptionManager contracts.EncryptionManager, store contracts.EncryptedValueStorage, + migrationExecutor contracts.EncryptedValueMigrationExecutor, reg prometheus.Registerer, -) *SQLKeeper { + cfg *setting.Cfg, +) (*SQLKeeper, error) { + // Only run the migration if running as an MT api server + if cfg.SecretsManagement.RunDataKeyMigration && cfg.SecretsManagement.RunSecretsDBMigrations { + // Run the encrypted value store migration before anything else, otherwise operations may fail + // TODO: This does not need to be here forever, but we may currently have on-prem deployments using GSM, so it needs to be here for now. + // Periodically assess whether it is safe to remove - most likely for G13 should be fine. + log := logging.FromContext(context.Background()) + log.Debug("sqlkeeper: executing encrypted value store migration") + rowsAffected, err := migrationExecutor.Execute(context.Background()) + log.Debug("sqlkeeper: encrypted value store migration completed", "rows_affected", rowsAffected) + if err != nil { + return nil, fmt.Errorf("error encountered during encrypted value store migration: %w", err) + } + } + return &SQLKeeper{ tracer: tracer, encryptionManager: encryptionManager, store: store, metrics: metrics.NewKeeperMetrics(reg), - } + }, nil } -func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (contracts.ExternalID, error) { +func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) (contracts.ExternalID, error) { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Store", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version)), ) @@ -63,9 +82,9 @@ func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, n return contracts.ExternalID(""), nil } -func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) { +func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Expose", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -77,7 +96,7 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, return "", fmt.Errorf("unable to get encrypted value: %w", err) } - exposedBytes, err := s.encryptionManager.Decrypt(ctx, namespace, encryptedValue.EncryptedData) + exposedBytes, err := s.encryptionManager.Decrypt(ctx, namespace, encryptedValue.EncryptedPayload) if err != nil { return "", fmt.Errorf("unable to decrypt value: %w", err) } @@ -88,9 +107,9 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, return exposedValue, nil } -func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error { +func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Delete", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -107,9 +126,9 @@ func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, return nil } -func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error { +func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace xkube.Namespace, name string, version int64, exposedValueOrRef string) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Update", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go index 1333cf894c7..34249227790 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go @@ -1,6 +1,7 @@ package sqlkeeper_test import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -8,6 +9,7 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -16,10 +18,10 @@ func TestMain(m *testing.M) { } func Test_SQLKeeperSetup(t *testing.T) { - namespace1 := "namespace1" + namespace1 := xkube.Namespace("namespace1") name1 := "name1" version1 := int64(1) - namespace2 := "namespace2" + namespace2 := xkube.Namespace("namespace2") name2 := "name2" plaintext1 := "very secret string in namespace 1" plaintext2 := "very secret string in namespace 2" @@ -145,4 +147,47 @@ func Test_SQLKeeperSetup(t *testing.T) { err = sut.SQLKeeper.Update(t.Context(), nil, namespace1, "non_existing_name", version1, plaintext2) require.Error(t, err) }) + + t.Run("data key migration only runs if both secrets db migrations are enabled", func(t *testing.T) { + t.Parallel() + + m := &mockMigrationExecutor{} + + testutils.Setup(t, testutils.WithMutateCfg(func(cfg *testutils.SetupConfig) { + cfg.RunSecretsDBMigrations = false + cfg.RunDataKeyMigration = false + cfg.DataKeyMigrationExecutor = m + })) + assert.False(t, m.wasExecuted) + + testutils.Setup(t, testutils.WithMutateCfg(func(cfg *testutils.SetupConfig) { + cfg.RunSecretsDBMigrations = true + cfg.RunDataKeyMigration = false + cfg.DataKeyMigrationExecutor = m + })) + assert.False(t, m.wasExecuted) + + testutils.Setup(t, testutils.WithMutateCfg(func(cfg *testutils.SetupConfig) { + cfg.RunSecretsDBMigrations = false + cfg.RunDataKeyMigration = true + cfg.DataKeyMigrationExecutor = m + })) + assert.False(t, m.wasExecuted) + + testutils.Setup(t, testutils.WithMutateCfg(func(cfg *testutils.SetupConfig) { + cfg.RunSecretsDBMigrations = true + cfg.RunDataKeyMigration = true + cfg.DataKeyMigrationExecutor = m + })) + assert.True(t, m.wasExecuted) + }) +} + +type mockMigrationExecutor struct { + wasExecuted bool +} + +func (m *mockMigrationExecutor) Execute(ctx context.Context) (int, error) { + m.wasExecuted = true + return 0, nil } diff --git a/pkg/registry/apis/secret/service/consolidation.go b/pkg/registry/apis/secret/service/consolidation.go index b0baea659c4..bbe7d9084e6 100644 --- a/pkg/registry/apis/secret/service/consolidation.go +++ b/pkg/registry/apis/secret/service/consolidation.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" otelcodes "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) @@ -60,21 +61,21 @@ func (s *ConsolidationService) Consolidate(ctx context.Context) (err error) { for _, ev := range encryptedValues { // Decrypt the value using its old data key. - decryptedValue, err := s.encryptionManager.Decrypt(ctx, ev.Namespace, ev.EncryptedData) + decryptedValue, err := s.encryptionManager.Decrypt(ctx, xkube.Namespace(ev.Namespace), ev.EncryptedPayload) if err != nil { logging.FromContext(ctx).Error("Failed to decrypt value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue } // Re-encrypt the value using a new data key. - reEncryptedValue, err := s.encryptionManager.Encrypt(ctx, ev.Namespace, decryptedValue) + reEncryptedValue, err := s.encryptionManager.Encrypt(ctx, xkube.Namespace(ev.Namespace), decryptedValue) if err != nil { logging.FromContext(ctx).Error("Failed to re-encrypt value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue } // Update the encrypted value in the store. - err = s.encryptedValueStore.Update(ctx, ev.Namespace, ev.Name, ev.Version, reEncryptedValue) + err = s.encryptedValueStore.Update(ctx, xkube.Namespace(ev.Namespace), ev.Name, ev.Version, reEncryptedValue) if err != nil { logging.FromContext(ctx).Error("Failed to update encrypted value", "namespace", ev.Namespace, "name", ev.Name, "error", err) continue diff --git a/pkg/registry/apis/secret/service/consolidation_test.go b/pkg/registry/apis/secret/service/consolidation_test.go index 638ced19a81..381cf956436 100644 --- a/pkg/registry/apis/secret/service/consolidation_test.go +++ b/pkg/registry/apis/secret/service/consolidation_test.go @@ -97,7 +97,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) originalDecryptedValues = append(originalDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotNil(t, encryptedValue) originalEncryptedData = append(originalEncryptedData, encryptedValue.EncryptedData) @@ -115,7 +115,7 @@ func TestConsolidation(t *testing.T) { require.Equal(t, originalDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) // Verify that the encrypted data has changed (indicating re-encryption) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, originalEncryptedData[i], encryptedValue.EncryptedData) } @@ -174,7 +174,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) initialDecryptedValues = append(initialDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) initialEncryptedData = append(initialEncryptedData, encryptedValue.EncryptedData) } @@ -223,7 +223,7 @@ func TestConsolidation(t *testing.T) { require.NoError(t, err) newSecretDecryptedValues = append(newSecretDecryptedValues, decryptedValue.DangerouslyExposeAndConsumeValue()) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) newSecretEncryptedData = append(newSecretEncryptedData, encryptedValue.EncryptedData) } @@ -252,7 +252,7 @@ func TestConsolidation(t *testing.T) { require.Equal(t, initialDecryptedValues[i], decryptedValue.DangerouslyExposeAndConsumeValue()) // Verify that the encrypted data has changed (indicating re-encryption) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, initialEncryptedData[i], encryptedValue.EncryptedData) } @@ -275,7 +275,7 @@ func TestConsolidation(t *testing.T) { // Verify that the encrypted data has changed from what it was when first created // (indicating it was re-encrypted during consolidation) - encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, tc.namespace, tc.name, 1) + encryptedValue, err := sut.EncryptedValueStorage.Get(ctx, xkube.Namespace(tc.namespace), tc.name, 1) require.NoError(t, err) require.NotEqual(t, newSecretEncryptedData[i], encryptedValue.EncryptedData) } diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 2ad69b759db..cc4e6c80c72 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -146,7 +146,7 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv } logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type()) - secret, err := keeper.Expose(ctx, keeperCfg, newSecureValue.Namespace, newSecureValue.Name, currentVersion.Status.Version) + secret, err := keeper.Expose(ctx, keeperCfg, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, currentVersion.Status.Version) if err != nil { return nil, false, fmt.Errorf("reading secret value from keeper: %w", err) } @@ -191,7 +191,7 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv1b // TODO: can we stop using external id? // TODO: store uses only the namespace and returns and id. It could be a kv instead. // TODO: check that the encrypted store works with multiple versions - externalID, err := keeper.Store(ctx, keeperCfg, createdSv.Namespace, createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) + externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) if err != nil { return nil, fmt.Errorf("storing secure value in keeper: %w", err) } diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 50190dbb3ff..6a5f1e40d30 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -40,7 +40,10 @@ import ( ) type SetupConfig struct { - KeeperService contracts.KeeperService + KeeperService contracts.KeeperService + DataKeyMigrationExecutor contracts.EncryptedValueMigrationExecutor + RunSecretsDBMigrations bool + RunDataKeyMigration bool } func defaultSetupCfg() SetupConfig { @@ -92,6 +95,8 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { CurrentEncryptionProvider: "secret_key.v1", ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}}, GCWorkerEnabled: false, + RunSecretsDBMigrations: setupCfg.RunSecretsDBMigrations, + RunDataKeyMigration: setupCfg.RunDataKeyMigration, GCWorkerMaxBatchSize: 2, GCWorkerMaxConcurrentCleanups: 2, } @@ -126,7 +131,17 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { globalEncryptedValueStorage, err := encryptionstorage.ProvideGlobalEncryptedValueStorage(database, tracer) require.NoError(t, err) - sqlKeeper := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, nil) + // Initialize a noop migration executor for the sql keeper so it doesn't interfere with initialization, or use the one provided + fakeMigrationExecutor := setupCfg.DataKeyMigrationExecutor + if fakeMigrationExecutor == nil { + fakeMigrationExecutor = &NoopMigrationExecutor{} + } + sqlKeeper, err := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, fakeMigrationExecutor, nil, cfg) + require.NoError(t, err) + + // Initialize a real migration executor for test + realMigrationExecutor, err := encryptionstorage.ProvideEncryptedValueMigrationExecutor(database, tracer, encryptedValueStorage, globalEncryptedValueStorage) + require.NoError(t, err) var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper) @@ -158,39 +173,41 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { keeperService) return Sut{ - SecureValueService: secureValueService, - SecureValueMetadataStorage: secureValueMetadataStorage, - DecryptStorage: decryptStorage, - DecryptService: decryptService, - EncryptedValueStorage: encryptedValueStorage, - GlobalEncryptedValueStorage: globalEncryptedValueStorage, - SQLKeeper: sqlKeeper, - Database: database, - AccessClient: accessClient, - ConsolidationService: consolidationService, - EncryptionManager: encryptionManager, - GlobalDataKeyStore: globalDataKeyStore, - GarbageCollectionWorker: garbageCollectionWorker, - Clock: clock, - KeeperService: keeperService, - KeeperMetadataStorage: keeperMetadataStorage, + SecureValueService: secureValueService, + SecureValueMetadataStorage: secureValueMetadataStorage, + DecryptStorage: decryptStorage, + DecryptService: decryptService, + EncryptedValueStorage: encryptedValueStorage, + GlobalEncryptedValueStorage: globalEncryptedValueStorage, + EncryptedValueMigrationExecutor: realMigrationExecutor, + SQLKeeper: sqlKeeper, + Database: database, + AccessClient: accessClient, + ConsolidationService: consolidationService, + EncryptionManager: encryptionManager, + GlobalDataKeyStore: globalDataKeyStore, + GarbageCollectionWorker: garbageCollectionWorker, + Clock: clock, + KeeperService: keeperService, + KeeperMetadataStorage: keeperMetadataStorage, } } type Sut struct { - SecureValueService contracts.SecureValueService - SecureValueMetadataStorage contracts.SecureValueMetadataStorage - DecryptStorage contracts.DecryptStorage - DecryptService decryptcontracts.DecryptService - EncryptedValueStorage contracts.EncryptedValueStorage - GlobalEncryptedValueStorage contracts.GlobalEncryptedValueStorage - SQLKeeper *sqlkeeper.SQLKeeper - Database *database.Database - AccessClient types.AccessClient - ConsolidationService contracts.ConsolidationService - EncryptionManager contracts.EncryptionManager - GlobalDataKeyStore contracts.GlobalDataKeyStorage - GarbageCollectionWorker *garbagecollectionworker.Worker + SecureValueService contracts.SecureValueService + SecureValueMetadataStorage contracts.SecureValueMetadataStorage + DecryptStorage contracts.DecryptStorage + DecryptService decryptcontracts.DecryptService + EncryptedValueStorage contracts.EncryptedValueStorage + GlobalEncryptedValueStorage contracts.GlobalEncryptedValueStorage + EncryptedValueMigrationExecutor contracts.EncryptedValueMigrationExecutor + SQLKeeper *sqlkeeper.SQLKeeper + Database *database.Database + AccessClient types.AccessClient + ConsolidationService contracts.ConsolidationService + EncryptionManager contracts.EncryptionManager + GlobalDataKeyStore contracts.GlobalDataKeyStorage + GarbageCollectionWorker *garbagecollectionworker.Worker // The fake clock passed to implementations to make testing easier Clock *FakeClock KeeperService contracts.KeeperService @@ -366,3 +383,10 @@ func (c *FakeClock) Now() time.Time { func (c *FakeClock) AdvanceBy(duration time.Duration) { c.Current = c.Current.Add(duration) } + +type NoopMigrationExecutor struct { +} + +func (e *NoopMigrationExecutor) Execute(ctx context.Context) (int, error) { + return 0, nil +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index b753747d478..58e22810c7b 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -28,6 +28,7 @@ var WireSetExts = wire.NewSet( wire.Bind(new(iam.CoreRoleStorageBackend), new(*noopstorage.StorageBackendImpl)), wire.Bind(new(iam.RoleStorageBackend), new(*noopstorage.StorageBackendImpl)), wire.Bind(new(iam.RoleBindingStorageBackend), new(*noopstorage.StorageBackendImpl)), + wire.Bind(new(iam.ExternalGroupMappingStorageBackend), new(*noopstorage.StorageBackendImpl)), ) var provisioningExtras = wire.NewSet( @@ -49,6 +50,7 @@ var WireSet = wire.NewSet( // Secrets secret.RegisterDependencies, // Provisioning + provisioning.RegisterDependencies, provisioningExtras, // Each must be added here *and* in the ServiceSink above diff --git a/pkg/registry/apps/alerting/rules/alertrule/compat.go b/pkg/registry/apps/alerting/rules/alertrule/compat.go index 0ea9507c6e0..3b780f0c1d7 100644 --- a/pkg/registry/apps/alerting/rules/alertrule/compat.go +++ b/pkg/registry/apps/alerting/rules/alertrule/compat.go @@ -128,6 +128,10 @@ func convertToK8sResource( return nil, fmt.Errorf("failed to get metadata: %w", err) } meta.SetFolder(rule.NamespaceUID) + // Keep metadata label in sync with folder annotation for downstream consumers + if rule.NamespaceUID != "" { + k8sRule.Labels[model.FolderLabelKey] = rule.NamespaceUID + } if rule.UpdatedBy != nil { meta.SetUpdatedBy(string(*rule.UpdatedBy)) k8sRule.SetUpdatedBy(string(*rule.UpdatedBy)) diff --git a/pkg/registry/apps/alerting/rules/alertrule/legacy_storage.go b/pkg/registry/apps/alerting/rules/alertrule/legacy_storage.go index 1e2de84b784..13a7d0b01f7 100644 --- a/pkg/registry/apps/alerting/rules/alertrule/legacy_storage.go +++ b/pkg/registry/apps/alerting/rules/alertrule/legacy_storage.go @@ -126,11 +126,6 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createVa if p.GenerateName != "" { return nil, fmt.Errorf("generate-name is not supported in legacy storage mode") } - // TODO: move this to the validation function - if p.Labels[model.GroupLabelKey] != "" || p.Labels[model.GroupIndexLabelKey] != "" { - return nil, k8serrors.NewBadRequest("cannot set group when creating alert rule") - } - model, provenance, err := convertToDomainModel(info.OrgID, p) if err != nil { return nil, err @@ -160,12 +155,6 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up return old, false, err } - current, ok := old.(*model.AlertRule) - if !ok { - // this shouldn't really be possible - return nil, false, k8serrors.NewBadRequest("expected valid alert rule object") - } - obj, err := objInfo.UpdatedObject(ctx, old) if err != nil { return old, false, err @@ -180,9 +169,6 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up if !ok { return nil, false, k8serrors.NewBadRequest("expected valid alert rule object") } - if current.Labels[model.GroupLabelKey] == "" && new.Labels[model.GroupLabelKey] != "" { - return nil, false, k8serrors.NewBadRequest("cannot set group label when updating un-grouped alert rule") - } model, provenance, err := convertToDomainModel(info.OrgID, new) if err != nil { diff --git a/pkg/registry/apps/alerting/rules/recordingrule/compat.go b/pkg/registry/apps/alerting/rules/recordingrule/compat.go index e9ef0c4e295..8626a0b7e91 100644 --- a/pkg/registry/apps/alerting/rules/recordingrule/compat.go +++ b/pkg/registry/apps/alerting/rules/recordingrule/compat.go @@ -76,6 +76,10 @@ func convertToK8sResource( return nil, fmt.Errorf("failed to get metadata: %w", err) } meta.SetFolder(rule.NamespaceUID) + // Keep metadata label in sync with folder annotation for downstream consumers + if rule.NamespaceUID != "" { + k8sRule.Labels[model.FolderLabelKey] = rule.NamespaceUID + } if rule.UpdatedBy != nil { meta.SetUpdatedBy(string(*rule.UpdatedBy)) k8sRule.SetUpdatedBy(string(*rule.UpdatedBy)) diff --git a/pkg/registry/apps/alerting/rules/recordingrule/legacy_storage.go b/pkg/registry/apps/alerting/rules/recordingrule/legacy_storage.go index 395b955e0d0..cb69a51ce19 100644 --- a/pkg/registry/apps/alerting/rules/recordingrule/legacy_storage.go +++ b/pkg/registry/apps/alerting/rules/recordingrule/legacy_storage.go @@ -104,7 +104,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, _ *metav1.GetOptio return obj, err } -func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) { +func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) { info, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -114,6 +114,11 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, _ rest.V if err != nil { return nil, err } + if createValidation != nil { + if err := createValidation(ctx, obj); err != nil { + return nil, err + } + } p, ok := obj.(*model.RecordingRule) if !ok { @@ -157,11 +162,6 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up return nil, false, err } - current, ok := old.(*model.RecordingRule) - if !ok { - return nil, false, k8serrors.NewBadRequest("expected valid recording rule object") - } - obj, err := objInfo.UpdatedObject(ctx, old) if err != nil { return old, false, err @@ -180,10 +180,6 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up if new.Name != "" { new.UID = types.UID(new.Name) } - // TODO: move to validation function - if current.Labels[model.GroupLabelKey] == "" && new.Labels[model.GroupLabelKey] != "" { - return nil, false, k8serrors.NewBadRequest("cannot set group label when updating un-grouped recording rule") - } model, provenance, err := convertToDomainModel(info.OrgID, new) if err != nil { diff --git a/pkg/registry/apps/alerting/rules/register.go b/pkg/registry/apps/alerting/rules/register.go index 85b602ea832..d406495ac0e 100644 --- a/pkg/registry/apps/alerting/rules/register.go +++ b/pkg/registry/apps/alerting/rules/register.go @@ -14,13 +14,17 @@ import ( "github.com/grafana/grafana/apps/alerting/rules/pkg/apis" rulesApp "github.com/grafana/grafana/apps/alerting/rules/pkg/app" + rulesAppConfig "github.com/grafana/grafana/apps/alerting/rules/pkg/app/config" + "github.com/grafana/grafana/pkg/apimachinery/identity" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules/alertrule" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules/recordingrule" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + reqns "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/setting" ) @@ -50,11 +54,66 @@ func RegisterAppInstaller( ng: ng, } - provider := simple.NewAppProvider(apis.LocalManifest(), nil, rulesApp.New) + appSpecificConfig := rulesAppConfig.RuntimeConfig{ + // Validate folder existence using the folder service + FolderValidator: func(ctx context.Context, folderUID string) (bool, error) { + if folderUID == "" { + return false, nil + } + orgID, err := reqns.OrgIDForList(ctx) + user, _ := identity.GetRequester(ctx) + if (err != nil || orgID < 1) && user != nil { + orgID = user.GetOrgID() + } + if user == nil || orgID < 1 { + // If we can't resolve identity/org in this context, don't block creation based on existence + return true, nil + } + // Use the RuleStore to check namespace (folder) visibility + _, err = ng.Api.RuleStore.GetNamespaceByUID(ctx, folderUID, orgID, user) + if err != nil { + return false, nil + } + return true, nil + }, + BaseEvaluationInterval: ng.Cfg.UnifiedAlerting.BaseInterval, + ReservedLabelKeys: ngmodels.LabelsUserCannotSpecify, + // Validate that the configured notification receiver exists in the Alertmanager config + NotificationSettingsValidator: func(ctx context.Context, receiver string) (bool, error) { + if receiver == "" { + return false, nil + } + orgID, err := reqns.OrgIDForList(ctx) + if err != nil || orgID < 1 { + if user, _ := identity.GetRequester(ctx); user != nil { + orgID = user.GetOrgID() + } + } + if orgID < 1 { + // Without org context, skip validation rather than block + return true, nil + } + provider := notifier.NewCachedNotificationSettingsValidationService(ng.Api.AlertingStore) + vd, err := provider.Validator(ctx, orgID) + if err != nil { + log.New("alerting.rules.app").Error("failed to create notification settings validator", "error", err) + // If we cannot build a validator, don't block admission + return true, nil + } + // Only validate receiver presence; construct minimal settings + if err := vd.Validate(ngmodels.NotificationSettings{Receiver: receiver}); err != nil { + return false, nil + } + return true, nil + }, + } + + provider := simple.NewAppProvider(apis.LocalManifest(), appSpecificConfig, rulesApp.New) appConfig := app.Config{ - KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method - ManifestData: *apis.LocalManifest().ManifestData, + KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method + ManifestData: *apis.LocalManifest().ManifestData, + SpecificConfig: appSpecificConfig, } i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{}) @@ -81,7 +140,7 @@ func (a *AlertingRulesAppInstaller) GetAuthorizer() authorizer.Authorizer { } func (a *AlertingRulesAppInstaller) GetLegacyStorage(gvr schema.GroupVersionResource) grafanarest.Storage { - namespacer := request.GetNamespaceMapper(a.cfg) + namespacer := reqns.GetNamespaceMapper(a.cfg) switch gvr { case recordingrule.ResourceInfo.GroupVersionResource(): return recordingrule.NewStorage(*a.ng.Api.AlertRules, namespacer) diff --git a/pkg/registry/apps/annotation/register.go b/pkg/registry/apps/annotation/register.go new file mode 100644 index 00000000000..40d79df30ce --- /dev/null +++ b/pkg/registry/apps/annotation/register.go @@ -0,0 +1,344 @@ +package annotation + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apiserver/pkg/registry/rest" + restclient "k8s.io/client-go/rest" + + "github.com/grafana/grafana-app-sdk/app" + appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "github.com/grafana/grafana-app-sdk/simple" + "github.com/grafana/grafana/apps/annotation/pkg/apis" + annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1" + annotationapp "github.com/grafana/grafana/apps/annotation/pkg/app" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + apiserverrest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + _ appsdkapiserver.AppInstaller = (*AnnotationAppInstaller)(nil) + _ appinstaller.LegacyStorageProvider = (*AnnotationAppInstaller)(nil) +) + +type AnnotationAppInstaller struct { + appsdkapiserver.AppInstaller + cfg *setting.Cfg + legacy *legacyStorage +} + +func RegisterAppInstaller( + cfg *setting.Cfg, + features featuremgmt.FeatureToggles, + service annotations.Repository, +) (*AnnotationAppInstaller, error) { + installer := &AnnotationAppInstaller{ + cfg: cfg, + } + provider := simple.NewAppProvider(apis.LocalManifest(), nil, annotationapp.New) + + appConfig := app.Config{ + KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method + ManifestData: *apis.LocalManifest().ManifestData, + } + i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator()) + if err != nil { + return nil, err + } + installer.AppInstaller = i + + if service != nil { + installer.legacy = &legacyStorage{ + service: service, + namespacer: request.GetNamespaceMapper(cfg), + } + } + + return installer, nil +} + +func (a *AnnotationAppInstaller) GetLegacyStorage(requested schema.GroupVersionResource) apiserverrest.Storage { + kind := annotationV0.AnnotationKind() + gvr := schema.GroupVersionResource{ + Group: kind.Group(), + Version: kind.Version(), + Resource: kind.Plural(), + } + if requested.String() != gvr.String() { + return nil + } + a.legacy.tableConverter = utils.NewTableConverter( + gvr.GroupResource(), + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Text", Type: "string", Format: "name"}, + }, + Reader: func(obj any) ([]any, error) { + m, ok := obj.(*annotationV0.Annotation) + if !ok { + return nil, fmt.Errorf("expected Annotation") + } + return []any{ + m.Spec.Text, + }, nil + }, + }, + ) + + return a.legacy +} + +var ( + _ rest.Scoper = (*legacyStorage)(nil) + _ rest.SingularNameProvider = (*legacyStorage)(nil) + _ rest.Getter = (*legacyStorage)(nil) + _ rest.Storage = (*legacyStorage)(nil) + _ rest.Creater = (*legacyStorage)(nil) + _ rest.Updater = (*legacyStorage)(nil) + _ rest.GracefulDeleter = (*legacyStorage)(nil) +) + +type legacyStorage struct { + service annotations.Repository + namespacer request.NamespaceMapper + tableConverter rest.TableConvertor +} + +func (s *legacyStorage) New() runtime.Object { + return annotationV0.AnnotationKind().ZeroValue() +} + +func (s *legacyStorage) Destroy() {} + +func (s *legacyStorage) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *legacyStorage) GetSingularName() string { + return strings.ToLower(annotationV0.AnnotationKind().Kind()) +} + +func (s *legacyStorage) NewList() runtime.Object { + return annotationV0.AnnotationKind().ZeroListValue() +} + +func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + orgID, err := request.OrgIDForList(ctx) + if err != nil { + return nil, err + } + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + query := &annotations.ItemQuery{OrgID: orgID, SignedInUser: user, AlertID: -1} + if options.FieldSelector != nil { + for _, r := range options.FieldSelector.Requirements() { + switch r.Field { + case "spec.dashboardUID": + if r.Operator == selection.Equals || r.Operator == selection.DoubleEquals { + query.DashboardUID = r.Value + } else { + return nil, fmt.Errorf("unsupported operator %s for spec.dashboardUID (only = supported)", r.Operator) + } + + case "spec.panelID": + if r.Operator == selection.Equals || r.Operator == selection.DoubleEquals { + panelID, err := strconv.ParseInt(r.Value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid panelID value %q: %w", r.Value, err) + } + query.PanelID = panelID + } else { + return nil, fmt.Errorf("unsupported operator %s for spec.panelID (only = supported)", r.Operator) + } + case "spec.time": + switch r.Operator { + case selection.GreaterThan: + from, err := strconv.ParseInt(r.Value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid time value %q: %w", r.Value, err) + } + query.From = from + case selection.LessThan: + to, err := strconv.ParseInt(r.Value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid time value %q: %w", r.Value, err) + } + query.To = to + default: + return nil, fmt.Errorf("unsupported operator %s for spec.time (only >, < supported for ranges)", r.Operator) + } + + case "spec.timeEnd": + switch r.Operator { + case selection.GreaterThan: + from, err := strconv.ParseInt(r.Value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid timeEnd value %q: %w", r.Value, err) + } + query.From = from + case selection.LessThan: + to, err := strconv.ParseInt(r.Value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid timeEnd value %q: %w", r.Value, err) + } + query.To = to + default: + return nil, fmt.Errorf("unsupported operator %s for spec.timeEnd (only >, < supported for ranges)", r.Operator) + } + + default: + return nil, fmt.Errorf("unsupported field selector: %s", r.Field) + } + } + } + + query.Limit = 100 + if options.Limit > 0 { + query.Limit = options.Limit + } + items, err := s.service.Find(ctx, query) + if err != nil { + return nil, err + } + list := &annotationV0.AnnotationList{ + Items: make([]annotationV0.Annotation, len(items)), + } + for i, item := range items { + c, err := toK8sResource(orgID, item, s.namespacer) + if err != nil { + return nil, err + } + list.Items[i] = *c + } + + // TODO: pagination? + return list, nil +} + +func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + return nil, errors.New("fetching single annotations not supported by legacy storage") +} + +func (s *legacyStorage) Create(ctx context.Context, + obj runtime.Object, + createValidation rest.ValidateObjectFunc, + options *metav1.CreateOptions, +) (runtime.Object, error) { + return nil, errors.New("not implemented") + // resource, ok := obj.(*correlationsV0.Correlation) + // if !ok { + // return nil, fmt.Errorf("expected correlation") + // } + // + // cmd, err := correlations.ToCreateCorrelationCommand(resource) + // if err != nil { + // return nil, err + // } + // + // out, err := s.service.CreateCorrelation(ctx, *cmd) + // if err != nil { + // return nil, err + // } + // return s.Get(ctx, out.UID, &metav1.GetOptions{}) +} + +func (s *legacyStorage) Update(ctx context.Context, + name string, + objInfo rest.UpdatedObjectInfo, + createValidation rest.ValidateObjectFunc, + updateValidation rest.ValidateObjectUpdateFunc, + forceAllowCreate bool, + options *metav1.UpdateOptions, +) (runtime.Object, bool, error) { + return nil, false, errors.New("not implemented") + // before, err := s.Get(ctx, name, &metav1.GetOptions{}) + // if err != nil { + // return nil, false, err + // } + // obj, err := objInfo.UpdatedObject(ctx, before) + // if err != nil { + // return nil, false, err + // } + // + // resource, ok := obj.(*correlationsV0.Correlation) + // if !ok { + // return nil, false, fmt.Errorf("expected correlation") + // } + // + // cmd, err := correlations.ToUpdateCorrelationCommand(resource) + // if err != nil { + // return nil, false, err + // } + // + // out, err := s.service.UpdateCorrelation(ctx, *cmd) + // if err != nil { + // return nil, false, err + // } + // obj, err = s.Get(ctx, out.UID, &metav1.GetOptions{}) + // return obj, false, err +} + +// GracefulDeleter +func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + return nil, false, errors.New("not implemented") + // orgID, err := request.OrgIDForList(ctx) + // if err != nil { + // return nil, false, err + // } + // err = s.service.DeleteCorrelation(ctx, correlations.DeleteCorrelationCommand{ + // OrgId: orgID, + // UID: name, + // }) + // return nil, (err == nil), err +} + +// CollectionDeleter +func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { + return nil, fmt.Errorf("DeleteCollection for annotation not implemented") +} + +func toK8sResource(orgID int64, item *annotations.ItemDTO, namespacer request.NamespaceMapper) (*annotationV0.Annotation, error) { + annotation := &annotationV0.Annotation{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("a-%d", item.ID), // FIXME + Namespace: namespacer(orgID), + }, + Spec: annotationV0.AnnotationSpec{ + Text: item.Text, + Time: item.Time, + Tags: item.Tags, + }, + } + + if item.DashboardUID != nil && *item.DashboardUID != "" { + annotation.Spec.DashboardUID = item.DashboardUID + } + if item.PanelID != 0 { + annotation.Spec.PanelID = &item.PanelID + } + if item.TimeEnd != 0 { + annotation.Spec.TimeEnd = &item.TimeEnd + } + return annotation, nil +} diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index ad28e417e4a..5c039264a4b 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -14,7 +14,9 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/advisor" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" + "github.com/grafana/grafana/pkg/registry/apps/annotation" "github.com/grafana/grafana/pkg/registry/apps/correlations" + "github.com/grafana/grafana/pkg/registry/apps/example" "github.com/grafana/grafana/pkg/registry/apps/investigations" "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -38,10 +40,13 @@ func ProvideAppInstallers( correlationsAppInstaller *correlations.AppInstaller, alertingNotificationAppInstaller *notifications.AlertingNotificationsAppInstaller, logsdrilldownAppInstaller *logsdrilldown.LogsDrilldownAppInstaller, + annotationAppInstaller *annotation.AnnotationAppInstaller, + exampleAppInstaller *example.ExampleAppInstaller, ) []appsdkapiserver.AppInstaller { installers := []appsdkapiserver.AppInstaller{ playlistAppInstaller, pluginsApplInstaller, + exampleAppInstaller, } //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) { @@ -62,6 +67,11 @@ func ProvideAppInstallers( if features.IsEnabledGlobally(featuremgmt.FlagKubernetesLogsDrilldown) { installers = append(installers, logsdrilldownAppInstaller) } + //nolint:staticcheck + if features.IsEnabledGlobally(featuremgmt.FlagKubernetesAnnotations) { + installers = append(installers, annotationAppInstaller) + } + return installers } diff --git a/pkg/registry/apps/apps_test.go b/pkg/registry/apps/apps_test.go index 83c337636c0..3c92bda62ad 100644 --- a/pkg/registry/apps/apps_test.go +++ b/pkg/registry/apps/apps_test.go @@ -7,7 +7,9 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" + "github.com/grafana/grafana/pkg/registry/apps/annotation" "github.com/grafana/grafana/pkg/registry/apps/correlations" + "github.com/grafana/grafana/pkg/registry/apps/example" "github.com/grafana/grafana/pkg/registry/apps/playlist" "github.com/grafana/grafana/pkg/registry/apps/plugins" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -19,6 +21,8 @@ func TestProvideAppInstallers_Table(t *testing.T) { rulesInstaller := &rules.AlertingRulesAppInstaller{} correlationsAppInstaller := &correlations.AppInstaller{} notificationsAppInstaller := ¬ifications.AlertingNotificationsAppInstaller{} + annotationAppInstaller := &annotation.AnnotationAppInstaller{} + exampleAppInstaller := &example.ExampleAppInstaller{} tests := []struct { name string @@ -35,7 +39,7 @@ func TestProvideAppInstallers_Table(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { features := featuremgmt.WithFeatures(tt.flags...) - got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil) + got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, annotationAppInstaller, exampleAppInstaller) if tt.expectRulesApp { require.Contains(t, got, tt.rulesInst) } else { diff --git a/pkg/registry/apps/example/register.go b/pkg/registry/apps/example/register.go new file mode 100644 index 00000000000..1839d97b307 --- /dev/null +++ b/pkg/registry/apps/example/register.go @@ -0,0 +1,80 @@ +package example + +import ( + "fmt" + "strings" + + "k8s.io/apiserver/pkg/authorization/authorizer" + restclient "k8s.io/client-go/rest" + + "github.com/grafana/grafana-app-sdk/app" + appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "github.com/grafana/grafana-app-sdk/simple" + "github.com/grafana/grafana/apps/example/pkg/apis" + "github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1" + exampleapp "github.com/grafana/grafana/apps/example/pkg/app" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + _ appsdkapiserver.AppInstaller = (*ExampleAppInstaller)(nil) +) + +type ExampleAppInstaller struct { + appsdkapiserver.AppInstaller + cfg *setting.Cfg +} + +func (e ExampleAppInstaller) GetAuthorizer() authorizer.Authorizer { + return exampleapp.GetAuthorizer() +} + +func RegisterAppInstaller( + cfg *setting.Cfg, + features featuremgmt.FeatureToggles, +) (*ExampleAppInstaller, error) { + installer := &ExampleAppInstaller{ + cfg: cfg, + } + // Config specific to the app. This can pull from feature flags or setting.Cfg. + specificConfig := &exampleapp.ExampleConfig{ + EnableSomeFeature: true, + } + + // Set specificConfig.EnableReconciler to true IFF the v1alpha1 API is enabled in the runtime config. + // This is example-app-specific, as the version the reconciler uses is not served by default and must be enabled via an override. + apiserverRuntimeCfg := cfg.SectionWithEnvOverrides("grafana-apiserver").Key("runtime_config").String() + for _, s := range strings.Split(apiserverRuntimeCfg, ",") { + if len(s) == 0 { + continue + } + arr := strings.SplitN(s, "=", 2) + if len(arr) == 2 { + if arr[0] == fmt.Sprintf("%s/%s", v1alpha1.APIGroup, v1alpha1.APIVersion) { + specificConfig.EnableReconciler = strings.EqualFold("true", arr[1]) + break + } + } + } + + // Provider is the app provider, which contains the AppManifest, app-specific-config, and the New function for the app + provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, exampleapp.New) + + // appConfig is used alongside the provider for registrion. + // Most of the data is redunant, this may be more optimized in the future. + appConfig := app.Config{ + KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method + ManifestData: *apis.LocalManifest().ManifestData, + SpecificConfig: specificConfig, + } + // NewDefaultInstaller gets us the installer we need to underly the ExampleAppInstaller type. + // It does all the hard work of installing our app to the grafana API server + i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator()) + if err != nil { + return nil, err + } + installer.AppInstaller = i + + return installer, nil +} diff --git a/pkg/registry/apps/playlist/legacy_storage.go b/pkg/registry/apps/playlist/legacy_storage.go index 4f6affce640..8425035e093 100644 --- a/pkg/registry/apps/playlist/legacy_storage.go +++ b/pkg/registry/apps/playlist/legacy_storage.go @@ -10,7 +10,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/registry/rest" playlist "github.com/grafana/grafana/apps/playlist/pkg/apis/playlist/v0alpha1" @@ -87,10 +86,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge }) if err != nil || dto == nil { if errors.Is(err, playlistsvc.ErrPlaylistNotFound) || err == nil { - err = k8serrors.NewNotFound(schema.GroupResource{ - Group: playlist.PlaylistKind().Group(), - Resource: playlist.PlaylistKind().Plural(), - }, name) + err = k8serrors.NewNotFound(playlist.PlaylistKind().GroupVersionResource().GroupResource(), name) } return nil, err } diff --git a/pkg/registry/apps/playlist/register.go b/pkg/registry/apps/playlist/register.go index 67d6476401f..52bbc0210f9 100644 --- a/pkg/registry/apps/playlist/register.go +++ b/pkg/registry/apps/playlist/register.go @@ -65,11 +65,7 @@ func RegisterAppInstaller( // GetLegacyStorage returns the legacy storage for the playlist app. func (p *PlaylistAppInstaller) GetLegacyStorage(requested schema.GroupVersionResource) grafanarest.Storage { - gvr := schema.GroupVersionResource{ - Group: playlistv0alpha1.PlaylistKind().Group(), - Version: playlistv0alpha1.PlaylistKind().Version(), - Resource: playlistv0alpha1.PlaylistKind().Plural(), - } + gvr := playlistv0alpha1.PlaylistKind().GroupVersionResource() if requested.String() != gvr.String() { return nil } diff --git a/pkg/registry/apps/plugins/register.go b/pkg/registry/apps/plugins/register.go index dc05a47f965..e95b120cd66 100644 --- a/pkg/registry/apps/plugins/register.go +++ b/pkg/registry/apps/plugins/register.go @@ -1,20 +1,15 @@ package plugins import ( - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/registry/rest" restclient "k8s.io/client-go/rest" "github.com/grafana/grafana-app-sdk/app" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" "github.com/grafana/grafana-app-sdk/simple" pluginsappapis "github.com/grafana/grafana/apps/plugins/pkg/apis" - pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" ) @@ -26,16 +21,13 @@ var ( type PluginsAppInstaller struct { appsdkapiserver.AppInstaller - cfg *setting.Cfg } func RegisterAppInstaller( cfg *setting.Cfg, features featuremgmt.FeatureToggles, ) (*PluginsAppInstaller, error) { - installer := &PluginsAppInstaller{ - cfg: cfg, - } + installer := &PluginsAppInstaller{} specificConfig := any(nil) provider := simple.NewAppProvider(pluginsappapis.LocalManifest(), specificConfig, pluginsapp.New) appConfig := app.Config{ @@ -51,25 +43,6 @@ func RegisterAppInstaller( return installer, nil } -func (p *PluginsAppInstaller) InstallAPIs( - server appsdkapiserver.GenericAPIServer, - restOptsGetter generic.RESTOptionsGetter, -) error { - pluginMetaGVR := schema.GroupVersionResource{ - Group: pluginsv0alpha1.GroupVersion.Group, - Version: pluginsv0alpha1.GroupVersion.Version, - Resource: pluginsv0alpha1.PluginMetaKind().Plural(), - } - replacedStorage := map[schema.GroupVersionResource]rest.Storage{ - pluginMetaGVR: pluginsapp.NewPluginMetaStorage(request.GetNamespaceMapper(p.cfg)), - } - wrappedServer := &customStorageWrapper{ - wrapped: server, - replace: replacedStorage, - } - return p.AppInstaller.InstallAPIs(wrappedServer, restOptsGetter) -} - // GetAuthorizer returns the authorizer for the plugins app. func (p *PluginsAppInstaller) GetAuthorizer() authorizer.Authorizer { return pluginsapp.GetAuthorizer() diff --git a/pkg/registry/apps/shorturl/conversions.go b/pkg/registry/apps/shorturl/conversions.go index 979106d3bda..aa4a5c09d4f 100644 --- a/pkg/registry/apps/shorturl/conversions.go +++ b/pkg/registry/apps/shorturl/conversions.go @@ -8,7 +8,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" + shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/shorturls" @@ -21,11 +21,18 @@ func convertToK8sResource(v *shorturls.ShortUrl, namespacer request.NamespaceMap status := shorturl.ShortURLStatus{ LastSeenAt: v.LastSeenAt, } + + // resourceVersion can't be 0, since we are using the lastSeenAt value, when it's zero we default to current time + resourceVersion := fmt.Sprintf("%d", v.LastSeenAt) + if v.LastSeenAt == 0 { + resourceVersion = fmt.Sprintf("%d", time.Now().Unix()) + } + p := &shorturl.ShortURL{ ObjectMeta: metav1.ObjectMeta{ Name: v.Uid, - ResourceVersion: fmt.Sprintf("%d", v.LastSeenAt), - CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)), + ResourceVersion: resourceVersion, + CreationTimestamp: metav1.NewTime(time.Unix(v.CreatedAt, 0)), Namespace: namespacer(v.OrgId), }, Spec: spec, diff --git a/pkg/registry/apps/shorturl/legacy_storage.go b/pkg/registry/apps/shorturl/legacy_storage.go index 8ee7a59e899..3ad408b1b4b 100644 --- a/pkg/registry/apps/shorturl/legacy_storage.go +++ b/pkg/registry/apps/shorturl/legacy_storage.go @@ -10,16 +10,13 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/registry/rest" - shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" + shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/shorturls" - "github.com/grafana/grafana/pkg/services/user" ) var ( @@ -88,19 +85,10 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge return nil, err } - // Convert any identity.Requester to *user.SignedInUser - signedInUser, err := convertRequesterToSignedInUser(requester) - if err != nil { - return nil, fmt.Errorf("failed to convert requester: %w", err) - } - - dto, err := s.service.GetShortURLByUID(ctx, signedInUser, name) + dto, err := s.service.GetShortURLByUID(ctx, requester, name) if err != nil || dto == nil { if errors.Is(err, shorturls.ErrShortURLNotFound) || err == nil { - err = k8serrors.NewNotFound(schema.GroupResource{ - Group: shorturl.ShortURLKind().Group(), - Resource: shorturl.ShortURLKind().Plural(), - }, name) + err = k8serrors.NewNotFound(shorturl.ShortURLKind().GroupVersionResource().GroupResource(), name) } return nil, err } @@ -118,12 +106,6 @@ func (s *legacyStorage) Create(ctx context.Context, return nil, err } - // Convert any identity.Requester to *user.SignedInUser - signedInUser, err := convertRequesterToSignedInUser(requester) - if err != nil { - return nil, fmt.Errorf("failed to convert requester: %w", err) - } - if createValidation != nil { if err := createValidation(ctx, obj.DeepCopyObject()); err != nil { return nil, err @@ -137,7 +119,7 @@ func (s *legacyStorage) Create(ctx context.Context, Path: p.Spec.Path, UID: p.Name, } - out, err := s.service.CreateShortURL(ctx, signedInUser, cmd) + out, err := s.service.CreateShortURL(ctx, requester, cmd) if err != nil { return nil, err } @@ -158,19 +140,10 @@ func (s *legacyStorage) Update(ctx context.Context, return nil, false, err } - // Convert any identity.Requester to *user.SignedInUser - signedInUser, err := convertRequesterToSignedInUser(requester) - if err != nil { - return nil, false, fmt.Errorf("failed to convert requester: %w", err) - } - - shortURL, err := s.service.GetShortURLByUID(ctx, signedInUser, name) + shortURL, err := s.service.GetShortURLByUID(ctx, requester, name) if err != nil || shortURL == nil { if errors.Is(err, shorturls.ErrShortURLNotFound) || err == nil { - err = k8serrors.NewNotFound(schema.GroupResource{ - Group: shorturl.ShortURLKind().Group(), - Resource: shorturl.ShortURLKind().Plural(), - }, name) + err = k8serrors.NewNotFound(shorturl.ShortURLKind().GroupVersionResource().GroupResource(), name) } return nil, false, err } @@ -180,7 +153,7 @@ func (s *legacyStorage) Update(ctx context.Context, return nil, false, err } // Fetch the updated short URL to return - updatedLegacyShortURL, err := s.service.GetShortURLByUID(ctx, signedInUser, name) + updatedLegacyShortURL, err := s.service.GetShortURLByUID(ctx, requester, name) if err != nil { return nil, false, err } @@ -206,27 +179,3 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { return nil, fmt.Errorf("DeleteCollection for shorturl not implemented") } - -// convertRequesterToSignedInUser converts any identity.Requester to *user.SignedInUser -// This is needed because some legacy shorturls service methods still expect SignedInUser -func convertRequesterToSignedInUser(requester identity.Requester) (*user.SignedInUser, error) { - // If it's already a SignedInUser, return it directly - if signedInUser, ok := requester.(*user.SignedInUser); ok { - return signedInUser, nil - } - - // If it's a StaticRequester (service identity), convert it - if staticRequester, ok := requester.(*identity.StaticRequester); ok { - return &user.SignedInUser{ - UserID: staticRequester.UserID, // Used for CreatedBy field - OrgID: staticRequester.OrgID, // Used in SQL queries - }, nil - } - - // If it's an authn.Identity, use its SignedInUser method - if authnIdentity, ok := requester.(*authn.Identity); ok { - return authnIdentity.SignedInUser(), nil - } - - return nil, fmt.Errorf("unsupported identity type") -} diff --git a/pkg/registry/apps/shorturl/register.go b/pkg/registry/apps/shorturl/register.go index ecf56769e55..c5d314ff2bc 100644 --- a/pkg/registry/apps/shorturl/register.go +++ b/pkg/registry/apps/shorturl/register.go @@ -5,13 +5,14 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" restclient "k8s.io/client-go/rest" "github.com/grafana/grafana-app-sdk/app" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" "github.com/grafana/grafana-app-sdk/simple" "github.com/grafana/grafana/apps/shorturl/pkg/apis" - shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" + shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" shorturlapp "github.com/grafana/grafana/apps/shorturl/pkg/app" "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" @@ -55,11 +56,7 @@ func RegisterAppInstaller( } func (s *ShortURLAppInstaller) GetLegacyStorage(requested schema.GroupVersionResource) grafanarest.Storage { - gvr := schema.GroupVersionResource{ - Group: shorturl.ShortURLKind().Group(), - Version: shorturl.ShortURLKind().Version(), - Resource: shorturl.ShortURLKind().Plural(), - } + gvr := shorturl.ShortURLKind().GroupVersionResource() if requested.String() != gvr.String() { return nil } @@ -90,3 +87,18 @@ func (s *ShortURLAppInstaller) GetLegacyStorage(requested schema.GroupVersionRes ) return legacyStore } + +func (s *ShortURLAppInstaller) GetLegacyStatus(requested schema.GroupVersionResource, unified *appsdkapiserver.StatusREST) rest.Storage { + gvr := shorturl.ShortURLKind().GroupVersionResource() + if requested.String() != gvr.String() { + return nil + } + return &statusDualWriter{ + gv: gvr.GroupVersion(), + status: unified, + legacy: &legacyStorage{ + service: s.service, + namespacer: s.namespacer, + }, + } +} diff --git a/pkg/registry/apps/shorturl/status.go b/pkg/registry/apps/shorturl/status.go new file mode 100644 index 00000000000..2e50c3f6b78 --- /dev/null +++ b/pkg/registry/apps/shorturl/status.go @@ -0,0 +1,111 @@ +package shorturl + +import ( + "context" + "errors" + "fmt" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" + "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + + "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "github.com/grafana/grafana-app-sdk/logging" + shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/shorturls" +) + +type statusDualWriter struct { + gv schema.GroupVersion + status *apiserver.StatusREST + legacy *legacyStorage +} + +var ( + _ rest.Patcher = (*statusDualWriter)(nil) + _ rest.Storage = (*statusDualWriter)(nil) + _ rest.ResetFieldsStrategy = (*statusDualWriter)(nil) +) + +// Destroy implements rest.Storage. +func (s *statusDualWriter) Destroy() {} + +// New implements rest.Storage. +func (s *statusDualWriter) New() runtime.Object { + return s.legacy.New() +} + +// Get implements rest.Patcher. +func (s *statusDualWriter) Get(ctx context.Context, name string, options *v1.GetOptions) (runtime.Object, error) { + return s.legacy.Get(ctx, name, options) +} + +// Update implements rest.Patcher. +func (s *statusDualWriter) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *v1.UpdateOptions) (runtime.Object, bool, error) { + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, false, err + } + + shortURL, err := s.legacy.service.GetShortURLByUID(ctx, requester, name) + if err != nil || shortURL == nil { + if errors.Is(err, shorturls.ErrShortURLNotFound) || err == nil { + err = k8serrors.NewNotFound(shorturl.ShortURLKind().GroupVersionResource().GroupResource(), name) + } + return nil, false, err + } + + // This ignores the incoming and updates it directly + err = s.legacy.service.UpdateLastSeenAt(ctx, shortURL) + if err != nil { + return nil, false, err + } + + getter := func(getter rest.Getter) (*shorturl.ShortURL, error) { + obj, err := getter.Get(ctx, name, &v1.GetOptions{}) + if err != nil { + return nil, err + } + val, ok := obj.(*shorturl.ShortURL) + if !ok { + return nil, fmt.Errorf("expected ShortURL but got %T", obj) + } + return val, nil + } + + legacy, err := getter(s.legacy) + if err != nil { + return nil, false, err // unable to get legacy object + } + + unified, err := getter(s.status) + if err != nil { + logging.FromContext(ctx).Warn("unable to read unified status", "error", err) + return legacy, false, nil + } + + // Use the same status from legacy in unified + unified.Status = legacy.Status + + _, _, err = s.status.Update(ctx, name, rest.DefaultUpdatedObjectInfo(unified), createValidation, updateValidation, false, options) + if err != nil { + logging.FromContext(ctx).Warn("error updating unified status", "error", err) + } + + return legacy, false, err +} + +// GetResetFields implements rest.ResetFieldsStrategy +func (s *statusDualWriter) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + fields := map[fieldpath.APIVersion]*fieldpath.Set{ + fieldpath.APIVersion(s.gv.String()): fieldpath.NewSet( + fieldpath.MakePathOrDie("spec"), + fieldpath.MakePathOrDie("metadata"), + ), + } + return fields +} diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index b1bedf15ad2..60038502bf1 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -6,7 +6,9 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/advisor" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" + "github.com/grafana/grafana/pkg/registry/apps/annotation" "github.com/grafana/grafana/pkg/registry/apps/correlations" + "github.com/grafana/grafana/pkg/registry/apps/example" "github.com/grafana/grafana/pkg/registry/apps/investigations" "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -26,4 +28,6 @@ var WireSet = wire.NewSet( rules.RegisterAppInstaller, notifications.RegisterAppInstaller, logsdrilldown.RegisterAppInstaller, + annotation.RegisterAppInstaller, + example.RegisterAppInstaller, ) diff --git a/pkg/server/wire.go b/pkg/server/wire.go index d4a98e8a3c9..179b371760d 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -70,6 +70,7 @@ import ( "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn/authnimpl" "github.com/grafana/grafana/pkg/services/authz" + "github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/cloudmigration/cloudmigrationimpl" "github.com/grafana/grafana/pkg/services/contexthandler" @@ -431,6 +432,7 @@ var wireBasicSet = wire.NewSet( idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, + caching.ProvideCachingServiceClient, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), @@ -447,6 +449,7 @@ var wireBasicSet = wire.NewSet( secretencryption.ProvideGlobalDataKeyStorage, secretencryption.ProvideEncryptedValueStorage, secretencryption.ProvideGlobalEncryptedValueStorage, + secretencryption.ProvideEncryptedValueMigrationExecutor, secretsecurevalueservice.ProvideSecureValueService, secretvalidator.ProvideKeeperValidator, secretvalidator.ProvideSecureValueValidator, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 62435221671..75aca8fcc9a 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -80,7 +80,9 @@ import ( advisor2 "github.com/grafana/grafana/pkg/registry/apps/advisor" notifications2 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" + "github.com/grafana/grafana/pkg/registry/apps/annotation" correlations2 "github.com/grafana/grafana/pkg/registry/apps/correlations" + "github.com/grafana/grafana/pkg/registry/apps/example" "github.com/grafana/grafana/pkg/registry/apps/investigations" "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -218,7 +220,7 @@ import ( kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/services/secrets/migrator" + migrator2 "github.com/grafana/grafana/pkg/services/secrets/migrator" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" manager3 "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" @@ -256,7 +258,7 @@ import ( database4 "github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/grafana/grafana/pkg/storage/secret/metadata" - migrator2 "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" @@ -441,7 +443,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - zanzanaClient, err := authz.ProvideZanzana(cfg, sqlStore, tracingService, featureToggles, registerer) + zanzanaClient, err := authz.ProvideZanzanaClient(cfg, sqlStore, tracingService, featureToggles, registerer) if err != nil { return nil, err } @@ -484,7 +486,20 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + globalEncryptedValueStorage, err := encryption.ProvideGlobalEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return nil, err + } + encryptedValueMigrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(databaseDatabase, tracer, encryptedValueStorage, globalEncryptedValueStorage) + if err != nil { + return nil, err + } + secretDBMigrator := migrator.NewWithEngine(sqlStore) + dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, encryptedValueMigrationExecutor, registerer, cfg, dependencyRegisterer) if err != nil { return nil, err } @@ -582,7 +597,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } oauthtokenService := oauthtoken.ProvideService(socialService, authinfoimplService, cfg, registerer, serverLockService, tracingService, userAuthTokenService, featureToggles) ossCachingService := caching.ProvideCachingService() - middlewareHandler, err := pluginsintegration.ProvideClientWithMiddlewares(cfg, inMemory, oauthtokenService, tracingService, ossCachingService, featureToggles, registerer) + cachingServiceClient := caching.ProvideCachingServiceClient(ossCachingService, featureToggles) + middlewareHandler, err := pluginsintegration.ProvideClientWithMiddlewares(cfg, inMemory, oauthtokenService, tracingService, cachingServiceClient, featureToggles, registerer) if err != nil { return nil, err } @@ -697,7 +713,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) @@ -783,7 +799,15 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller) + annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl) + if err != nil { + return nil, err + } + exampleAppInstaller, err := example.RegisterAppInstaller(cfg, featureToggles) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { @@ -793,7 +817,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, featureToggles, plugincheckerService) + plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, plugincheckerService) if err != nil { return nil, err } @@ -821,15 +845,15 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) - dataSourceAPIBuilder, err := datasource.RegisterAPIService(configProvider, featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer) + dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService) if err != nil { return nil, err } folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, accessClient, registerer, resourceClient, zanzanaClient) storageBackendImpl := noopstorage.ProvideStorageBackend() - identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl, dualwriteService, resourceClient, userService) + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl, storageBackendImpl, dualwriteService, resourceClient, userService) if err != nil { return nil, err } @@ -869,12 +893,11 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - secretDBMigrator := migrator2.NewWithEngine(sqlStore) - dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) + provisioningDependencyRegisterer, err := provisioning2.RegisterDependencies(cfg, acimplService, featureToggles) if err != nil { return nil, err } - apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { return nil, err @@ -1056,7 +1079,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - zanzanaClient, err := authz.ProvideZanzana(cfg, sqlStore, tracingService, featureToggles, registerer) + zanzanaClient, err := authz.ProvideZanzanaClient(cfg, sqlStore, tracingService, featureToggles, registerer) if err != nil { return nil, err } @@ -1099,7 +1122,20 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + globalEncryptedValueStorage, err := encryption.ProvideGlobalEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return nil, err + } + encryptedValueMigrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor(databaseDatabase, tracer, encryptedValueStorage, globalEncryptedValueStorage) + if err != nil { + return nil, err + } + secretDBMigrator := migrator.NewWithEngine(sqlStore) + dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, encryptedValueMigrationExecutor, registerer, cfg, dependencyRegisterer) if err != nil { return nil, err } @@ -1189,7 +1225,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac service14 := service8.ProvideService(fileStoreManager, pluginService) oauthtokentestService := oauthtokentest.ProvideService() ossCachingService := caching.ProvideCachingService() - middlewareHandler, err := pluginsintegration.ProvideClientWithMiddlewares(cfg, inMemory, oauthtokentestService, tracingService, ossCachingService, featureToggles, registerer) + cachingServiceClient := caching.ProvideCachingServiceClient(ossCachingService, featureToggles) + middlewareHandler, err := pluginsintegration.ProvideClientWithMiddlewares(cfg, inMemory, oauthtokentestService, tracingService, cachingServiceClient, featureToggles, registerer) if err != nil { return nil, err } @@ -1314,7 +1351,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) @@ -1400,7 +1437,15 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller) + annotationAppInstaller, err := annotation.RegisterAppInstaller(cfg, featureToggles, repositoryImpl) + if err != nil { + return nil, err + } + exampleAppInstaller, err := example.RegisterAppInstaller(cfg, featureToggles) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { @@ -1410,7 +1455,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, featureToggles, plugincheckerService) + plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, plugincheckerService) if err != nil { return nil, err } @@ -1438,15 +1483,15 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) - dataSourceAPIBuilder, err := datasource.RegisterAPIService(configProvider, featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer) + dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService) if err != nil { return nil, err } folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, accessClient, registerer, resourceClient, zanzanaClient) storageBackendImpl := noopstorage.ProvideStorageBackend() - identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl, dualwriteService, resourceClient, userService) + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl, storageBackendImpl, dualwriteService, resourceClient, userService) if err != nil { return nil, err } @@ -1486,12 +1531,11 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - secretDBMigrator := migrator2.NewWithEngine(sqlStore) - dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) + provisioningDependencyRegisterer, err := provisioning2.RegisterDependencies(cfg, acimplService, featureToggles) if err != nil { return nil, err } - apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { return nil, err @@ -1560,7 +1604,7 @@ func InitializeForCLI(ctx context.Context, cfg *setting.Cfg) (Runner, error) { if err != nil { return Runner{}, err } - secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) configProvider, err := configprovider.ProvideService(cfg) if err != nil { return Runner{}, err @@ -1703,7 +1747,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)), annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, accesscontrol.ProvideFixedRolesLoader, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator2.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)), annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, accesscontrol.ProvideFixedRolesLoader, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, caching.ProvideCachingServiceClient, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/services/accesscontrol/dualwrite/collectors_test.go b/pkg/services/accesscontrol/dualwrite/collectors_test.go index 2c0ac88a689..aef03185330 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors_test.go +++ b/pkg/services/accesscontrol/dualwrite/collectors_test.go @@ -242,6 +242,18 @@ func (m *mockZanzanaClient) Compile(ctx context.Context, id authlib.AuthInfo, re return args.Get(0).(authlib.ItemChecker), args.Get(1).(authlib.Zookie), args.Error(2) } +func (m *mockZanzanaClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { + return nil +} + +func (m *mockZanzanaClient) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*authzextv1.QueryResponse), args.Error(1) +} + func TestIntegrationTeamMembershipCollector(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 3f93ce6ad82..dfda78ac9f0 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -73,11 +73,17 @@ func (r Role) MarshalJSON() ([]byte, error) { // swagger:ignore type RoleDTO struct { - Version int64 `json:"version"` - UID string `xorm:"uid" json:"uid"` - Name string `json:"name"` - DisplayName string `json:"displayName,omitempty"` - Description string `json:"description"` + // required:true + Version int64 `json:"version"` + // required:true + UID string `xorm:"uid" json:"uid"` + // required:true + Name string `json:"name"` + // required:true + DisplayName string `json:"displayName,omitempty"` + // required:true + Description string `json:"description"` + // required:true Group string `xorm:"group_name" json:"group"` Permissions []Permission `json:"permissions,omitempty"` Delegatable *bool `json:"delegatable,omitempty"` @@ -87,7 +93,9 @@ type RoleDTO struct { ID int64 `json:"-" xorm:"pk autoincr 'id'"` OrgID int64 `json:"-" xorm:"org_id"` + // required:true Updated time.Time `json:"updated"` + // required:true Created time.Time `json:"created"` } @@ -193,7 +201,7 @@ type BuiltinRole struct { Created time.Time } -// Permission is the model for access control permissions. +// Permission is the model for access control permissions type Permission struct { ID int64 `json:"-" xorm:"pk autoincr 'id'"` RoleID int64 `json:"-" xorm:"role_id"` diff --git a/pkg/services/annotations/annotationsimpl/xorm_store.go b/pkg/services/annotations/annotationsimpl/xorm_store.go index c59dc43f43f..da44dda4d99 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store.go @@ -330,7 +330,9 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query annotations.ItemQuer params = append(params, query.AnnotationID) } - if query.AlertID != 0 { + if query.AlertID < 0 { + sql.WriteString(` AND a.alert_id = 0`) + } else if query.AlertID > 0 { sql.WriteString(` AND a.alert_id = ?`) params = append(params, query.AlertID) } else if query.AlertUID != "" { diff --git a/pkg/services/apiserver/appinstaller/installer.go b/pkg/services/apiserver/appinstaller/installer.go index a89d2b84f9a..6397262d614 100644 --- a/pkg/services/apiserver/appinstaller/installer.go +++ b/pkg/services/apiserver/appinstaller/installer.go @@ -12,18 +12,18 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" serverstore "k8s.io/apiserver/pkg/server/storage" "k8s.io/kube-openapi/pkg/common" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" "github.com/grafana/grafana-app-sdk/logging" - "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) type LegacyStorageGetterFunc func(schema.GroupVersionResource) grafanarest.Storage @@ -32,6 +32,12 @@ type LegacyStorageProvider interface { GetLegacyStorage(schema.GroupVersionResource) grafanarest.Storage } +// In the rare case that that legacy needs to support the status subresource +// Unlike resource storage, dual writing must be managed explicitly +type LegacyStatusProvider interface { + GetLegacyStatus(schema.GroupVersionResource, *appsdkapiserver.StatusREST) rest.Storage +} + type AuthorizerProvider interface { GetAuthorizer() authorizer.Authorizer } @@ -197,6 +203,14 @@ func createPostStartHook( logger.Error("Failed to initialize app", "app", installer.ManifestData().AppName, "error", err) return fmt.Errorf("failed to get app from installer %s: %w", installer.ManifestData().AppName, err) } - return app.Runner().Run(hookContext.Context) + go func() { + err := app.Runner().Run(hookContext.Context) + if err != nil { + logger.Error("App runner exited with error", "app", installer.ManifestData().AppName, "error", err) + } else { + logger.Info("App runner exited without error", "app", installer.ManifestData().AppName) + } + }() + return nil } } diff --git a/pkg/services/apiserver/appinstaller/server.go b/pkg/services/apiserver/appinstaller/server.go index b6b32be48e9..ab688a46fb9 100644 --- a/pkg/services/apiserver/appinstaller/server.go +++ b/pkg/services/apiserver/appinstaller/server.go @@ -60,25 +60,41 @@ func (s *serverWrapper) InstallAPIGroup(apiGroupInfo *genericapiserver.APIGroupI continue } storage := s.configureStorage(gr, dualWriteSupported, restStorage) - if unifiedStorage, ok := storage.(grafanarest.Storage); ok && dualWriteSupported { - log.Debug("Configuring dual writer for storage", "resource", gr.String(), "version", v, "storagePath", storagePath) - dw, err := NewDualWriter( - s.ctx, - gr, - s.storageOpts, - legacyProvider.GetLegacyStorage(gr.WithVersion(v)), - unifiedStorage, - s.kvStore, - s.lock, - s.namespaceMapper, - s.dualWriteService, - s.dualWriterMetrics, - s.builderMetrics, - ) - if err != nil { - return err + if dualWriteSupported { + if unifiedStorage, ok := storage.(grafanarest.Storage); ok { + log.Debug("Configuring dual writer for storage", "resource", gr.String(), "version", v, "storagePath", storagePath) + storage, err = NewDualWriter( + s.ctx, + gr, + s.storageOpts, + legacyProvider.GetLegacyStorage(gr.WithVersion(v)), + unifiedStorage, + s.kvStore, + s.lock, + s.namespaceMapper, + s.dualWriteService, + s.dualWriterMetrics, + s.builderMetrics, + ) + if err != nil { + return err + } + } else if statusRest, ok := storage.(*appsdkapiserver.StatusREST); ok { + parentPath := strings.TrimSuffix(storagePath, "/status") + parentStore, ok := apiGroupInfo.VersionedResourcesStorageMap[v][parentPath] + if ok { + if _, isMode4or5 := parentStore.(*genericregistry.Store); !isMode4or5 { + // When legacy resources have status, the dual writing must be handled explicitly + if statusProvider, ok := s.installer.(LegacyStatusProvider); ok { + storage = statusProvider.GetLegacyStatus(gr.WithVersion(v), statusRest) + } else { + log.Warn("skipped registering status sub-resource that does not support dual writing", + "resource", gr.String(), "version", v, "storagePath", storagePath) + continue + } + } + } } - storage = dw } apiGroupInfo.VersionedResourcesStorageMap[v][storagePath] = storage } @@ -116,6 +132,13 @@ func (s *serverWrapper) configureStorage(gr schema.GroupResource, dualWriteSuppo return statusStore } + // if the storage is a subresource store, we need to extract the underlying generic registry store + if subresourceStore, ok := storage.(*appsdkapiserver.SubresourceREST); ok { + subresourceStore.Store.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr) + subresourceStore.Store.KeyRootFunc = grafanaregistry.KeyRootFunc(gr) + return subresourceStore + } + return storage } diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 222637401ee..cfcf3540b43 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -58,6 +58,12 @@ var PathRewriters = []filters.PathRewriter{ return matches[1] + "/name" // connector requires a name }, }, + { + Pattern: regexp.MustCompile(`(/apis/query.grafana.app/v0alpha1/namespaces/.*/sqlschemas$)`), + ReplaceFunc: func(matches []string) string { + return matches[1] + "/name" // connector requires a name + }, + }, { Pattern: regexp.MustCompile(`(/apis/.*/v0alpha1/namespaces/.*/queryconvert$)`), ReplaceFunc: func(matches []string) string { diff --git a/pkg/services/apiserver/builder/runner/admission_test.go b/pkg/services/apiserver/builder/runner/admission_test.go index f9b6fec3244..11275afdea8 100644 --- a/pkg/services/apiserver/builder/runner/admission_test.go +++ b/pkg/services/apiserver/builder/runner/admission_test.go @@ -5,12 +5,11 @@ import ( "errors" "testing" - "github.com/grafana/grafana-app-sdk/app" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authentication/user" + "github.com/grafana/grafana-app-sdk/app" examplev1 "github.com/grafana/grafana/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1" ) @@ -20,12 +19,8 @@ func TestBuilderAdmission_Validate(t *testing.T) { A: "test", }, } - gvk := schema.GroupVersionKind{ - Group: examplev1.ExampleKind().Group(), - Version: examplev1.ExampleKind().Version(), - Kind: examplev1.ExampleKind().Kind(), - } - gvr := gvk.GroupVersion().WithResource(examplev1.ExampleKind().Plural()) + gvk := examplev1.ExampleKind().GroupVersionKind() + gvr := examplev1.ExampleKind().GroupVersionResource() defaultAttributes := admission.NewAttributesRecord(exampleObj, nil, gvk, "default", "foo", gvr, "", admission.Create, nil, false, &user.DefaultInfo{}) tests := []struct { @@ -73,12 +68,8 @@ func TestBuilderAdmission_Validate(t *testing.T) { } func TestBuilderAdmission_Mutate(t *testing.T) { - gvk := schema.GroupVersionKind{ - Group: examplev1.ExampleKind().Group(), - Version: examplev1.ExampleKind().Version(), - Kind: examplev1.ExampleKind().Kind(), - } - gvr := gvk.GroupVersion().WithResource(examplev1.ExampleKind().Plural()) + gvk := examplev1.ExampleKind().GroupVersionKind() + gvr := examplev1.ExampleKind().GroupVersionResource() getAttributes := func() admission.Attributes { exampleObj := &examplev1.Example{ Spec: examplev1.ExampleSpec{ diff --git a/pkg/services/authz/proto/v1/extention.pb.go b/pkg/services/authz/proto/v1/extention.pb.go index 9f155187ab5..03a44978db9 100644 --- a/pkg/services/authz/proto/v1/extention.pb.go +++ b/pkg/services/authz/proto/v1/extention.pb.go @@ -24,6 +24,775 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type MutateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Operations []*MutateOperation `protobuf:"bytes,2,rep,name=operations,proto3" json:"operations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutateRequest) Reset() { + *x = MutateRequest{} + mi := &file_extention_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutateRequest) ProtoMessage() {} + +func (x *MutateRequest) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutateRequest.ProtoReflect.Descriptor instead. +func (*MutateRequest) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{0} +} + +func (x *MutateRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *MutateRequest) GetOperations() []*MutateOperation { + if x != nil { + return x.Operations + } + return nil +} + +type MutateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutateResponse) Reset() { + *x = MutateResponse{} + mi := &file_extention_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutateResponse) ProtoMessage() {} + +func (x *MutateResponse) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutateResponse.ProtoReflect.Descriptor instead. +func (*MutateResponse) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{1} +} + +type MutateOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Operation: + // + // *MutateOperation_SetFolderParent + // *MutateOperation_DeleteFolder + // *MutateOperation_CreatePermission + // *MutateOperation_DeletePermission + // *MutateOperation_UpdateUserOrgRole + // *MutateOperation_DeleteUserOrgRole + // *MutateOperation_AddUserOrgRole + Operation isMutateOperation_Operation `protobuf_oneof:"operation"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutateOperation) Reset() { + *x = MutateOperation{} + mi := &file_extention_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutateOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutateOperation) ProtoMessage() {} + +func (x *MutateOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutateOperation.ProtoReflect.Descriptor instead. +func (*MutateOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{2} +} + +func (x *MutateOperation) GetOperation() isMutateOperation_Operation { + if x != nil { + return x.Operation + } + return nil +} + +func (x *MutateOperation) GetSetFolderParent() *SetFolderParentOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_SetFolderParent); ok { + return x.SetFolderParent + } + } + return nil +} + +func (x *MutateOperation) GetDeleteFolder() *DeleteFolderOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeleteFolder); ok { + return x.DeleteFolder + } + } + return nil +} + +func (x *MutateOperation) GetCreatePermission() *CreatePermissionOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_CreatePermission); ok { + return x.CreatePermission + } + } + return nil +} + +func (x *MutateOperation) GetDeletePermission() *DeletePermissionOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeletePermission); ok { + return x.DeletePermission + } + } + return nil +} + +func (x *MutateOperation) GetUpdateUserOrgRole() *UpdateUserOrgRoleOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_UpdateUserOrgRole); ok { + return x.UpdateUserOrgRole + } + } + return nil +} + +func (x *MutateOperation) GetDeleteUserOrgRole() *DeleteUserOrgRoleOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeleteUserOrgRole); ok { + return x.DeleteUserOrgRole + } + } + return nil +} + +func (x *MutateOperation) GetAddUserOrgRole() *AddUserOrgRoleOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_AddUserOrgRole); ok { + return x.AddUserOrgRole + } + } + return nil +} + +type isMutateOperation_Operation interface { + isMutateOperation_Operation() +} + +type MutateOperation_SetFolderParent struct { + SetFolderParent *SetFolderParentOperation `protobuf:"bytes,1,opt,name=set_folder_parent,json=setFolderParent,proto3,oneof"` +} + +type MutateOperation_DeleteFolder struct { + DeleteFolder *DeleteFolderOperation `protobuf:"bytes,2,opt,name=delete_folder,json=deleteFolder,proto3,oneof"` +} + +type MutateOperation_CreatePermission struct { + CreatePermission *CreatePermissionOperation `protobuf:"bytes,3,opt,name=create_permission,json=createPermission,proto3,oneof"` +} + +type MutateOperation_DeletePermission struct { + DeletePermission *DeletePermissionOperation `protobuf:"bytes,4,opt,name=delete_permission,json=deletePermission,proto3,oneof"` +} + +type MutateOperation_UpdateUserOrgRole struct { + UpdateUserOrgRole *UpdateUserOrgRoleOperation `protobuf:"bytes,5,opt,name=update_user_org_role,json=updateUserOrgRole,proto3,oneof"` +} + +type MutateOperation_DeleteUserOrgRole struct { + DeleteUserOrgRole *DeleteUserOrgRoleOperation `protobuf:"bytes,6,opt,name=delete_user_org_role,json=deleteUserOrgRole,proto3,oneof"` +} + +type MutateOperation_AddUserOrgRole struct { + AddUserOrgRole *AddUserOrgRoleOperation `protobuf:"bytes,7,opt,name=add_user_org_role,json=addUserOrgRole,proto3,oneof"` +} + +func (*MutateOperation_SetFolderParent) isMutateOperation_Operation() {} + +func (*MutateOperation_DeleteFolder) isMutateOperation_Operation() {} + +func (*MutateOperation_CreatePermission) isMutateOperation_Operation() {} + +func (*MutateOperation_DeletePermission) isMutateOperation_Operation() {} + +func (*MutateOperation_UpdateUserOrgRole) isMutateOperation_Operation() {} + +func (*MutateOperation_DeleteUserOrgRole) isMutateOperation_Operation() {} + +func (*MutateOperation_AddUserOrgRole) isMutateOperation_Operation() {} + +type SetFolderParentOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UID of the folder + Folder string `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder,omitempty"` + // UID of the parent folder + Parent string `protobuf:"bytes,2,opt,name=parent,proto3" json:"parent,omitempty"` + // If true, delete all existing parent relations associated with the folder + DeleteExisting bool `protobuf:"varint,3,opt,name=delete_existing,json=deleteExisting,proto3" json:"delete_existing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetFolderParentOperation) Reset() { + *x = SetFolderParentOperation{} + mi := &file_extention_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetFolderParentOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetFolderParentOperation) ProtoMessage() {} + +func (x *SetFolderParentOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetFolderParentOperation.ProtoReflect.Descriptor instead. +func (*SetFolderParentOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{3} +} + +func (x *SetFolderParentOperation) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +func (x *SetFolderParentOperation) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *SetFolderParentOperation) GetDeleteExisting() bool { + if x != nil { + return x.DeleteExisting + } + return false +} + +type DeleteFolderOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UID of the folder to delete + Folder string `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder,omitempty"` + // UID of the parent folder + Parent string `protobuf:"bytes,2,opt,name=parent,proto3" json:"parent,omitempty"` + // If true, delete all existing parent relations associated with the folder + DeleteExisting bool `protobuf:"varint,3,opt,name=delete_existing,json=deleteExisting,proto3" json:"delete_existing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteFolderOperation) Reset() { + *x = DeleteFolderOperation{} + mi := &file_extention_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteFolderOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteFolderOperation) ProtoMessage() {} + +func (x *DeleteFolderOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteFolderOperation.ProtoReflect.Descriptor instead. +func (*DeleteFolderOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{4} +} + +func (x *DeleteFolderOperation) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +func (x *DeleteFolderOperation) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *DeleteFolderOperation) GetDeleteExisting() bool { + if x != nil { + return x.DeleteExisting + } + return false +} + +type CreatePermissionOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Resource *Resource `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + Permission *Permission `protobuf:"bytes,2,opt,name=permission,proto3" json:"permission,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreatePermissionOperation) Reset() { + *x = CreatePermissionOperation{} + mi := &file_extention_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreatePermissionOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePermissionOperation) ProtoMessage() {} + +func (x *CreatePermissionOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreatePermissionOperation.ProtoReflect.Descriptor instead. +func (*CreatePermissionOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{5} +} + +func (x *CreatePermissionOperation) GetResource() *Resource { + if x != nil { + return x.Resource + } + return nil +} + +func (x *CreatePermissionOperation) GetPermission() *Permission { + if x != nil { + return x.Permission + } + return nil +} + +type DeletePermissionOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Resource *Resource `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + Permission *Permission `protobuf:"bytes,2,opt,name=permission,proto3" json:"permission,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletePermissionOperation) Reset() { + *x = DeletePermissionOperation{} + mi := &file_extention_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletePermissionOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletePermissionOperation) ProtoMessage() {} + +func (x *DeletePermissionOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeletePermissionOperation.ProtoReflect.Descriptor instead. +func (*DeletePermissionOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{6} +} + +func (x *DeletePermissionOperation) GetResource() *Resource { + if x != nil { + return x.Resource + } + return nil +} + +func (x *DeletePermissionOperation) GetPermission() *Permission { + if x != nil { + return x.Permission + } + return nil +} + +type AddUserOrgRoleOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // User UID + User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Role name (e.g: "Admin", "Editor", "Viewer") + Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddUserOrgRoleOperation) Reset() { + *x = AddUserOrgRoleOperation{} + mi := &file_extention_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddUserOrgRoleOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddUserOrgRoleOperation) ProtoMessage() {} + +func (x *AddUserOrgRoleOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddUserOrgRoleOperation.ProtoReflect.Descriptor instead. +func (*AddUserOrgRoleOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{7} +} + +func (x *AddUserOrgRoleOperation) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *AddUserOrgRoleOperation) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +// UpdateUserOrgRoleOperation assigns the user's basic role and deletes existing basic role assignments. +type UpdateUserOrgRoleOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // User UID + User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Role name (e.g: "Admin", "Editor", "Viewer") + Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserOrgRoleOperation) Reset() { + *x = UpdateUserOrgRoleOperation{} + mi := &file_extention_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserOrgRoleOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserOrgRoleOperation) ProtoMessage() {} + +func (x *UpdateUserOrgRoleOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserOrgRoleOperation.ProtoReflect.Descriptor instead. +func (*UpdateUserOrgRoleOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{8} +} + +func (x *UpdateUserOrgRoleOperation) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *UpdateUserOrgRoleOperation) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +type DeleteUserOrgRoleOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // User UID + User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Role name (e.g: "Admin", "Editor", "Viewer") + Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteUserOrgRoleOperation) Reset() { + *x = DeleteUserOrgRoleOperation{} + mi := &file_extention_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserOrgRoleOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserOrgRoleOperation) ProtoMessage() {} + +func (x *DeleteUserOrgRoleOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserOrgRoleOperation.ProtoReflect.Descriptor instead. +func (*DeleteUserOrgRoleOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteUserOrgRoleOperation) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *DeleteUserOrgRoleOperation) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +type Resource struct { + state protoimpl.MessageState `protogen:"open.v1"` + // group of the resource (e.g: "dashboard.grafana.app") + Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` + // kind of the resource (e.g: "dashboards") + Resource string `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + // uid of the resource + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Resource) Reset() { + *x = Resource{} + mi := &file_extention_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Resource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Resource) ProtoMessage() {} + +func (x *Resource) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Resource.ProtoReflect.Descriptor instead. +func (*Resource) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{10} +} + +func (x *Resource) GetGroup() string { + if x != nil { + return x.Group + } + return "" +} + +func (x *Resource) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +func (x *Resource) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type Permission struct { + state protoimpl.MessageState `protogen:"open.v1"` + // kind of the identity getting the permission (e.g: "user", "team", "serviceaccount") + Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` + // uid of the identity getting the permission + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // action set granted to the user (e.g. "admin" or "edit", "view") + Verb string `protobuf:"bytes,3,opt,name=verb,proto3" json:"verb,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Permission) Reset() { + *x = Permission{} + mi := &file_extention_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Permission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Permission) ProtoMessage() {} + +func (x *Permission) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Permission.ProtoReflect.Descriptor instead. +func (*Permission) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{11} +} + +func (x *Permission) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *Permission) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Permission) GetVerb() string { + if x != nil { + return x.Verb + } + return "" +} + type TupleKey struct { state protoimpl.MessageState `protogen:"open.v1"` User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` @@ -36,7 +805,7 @@ type TupleKey struct { func (x *TupleKey) Reset() { *x = TupleKey{} - mi := &file_extention_proto_msgTypes[0] + mi := &file_extention_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -48,7 +817,7 @@ func (x *TupleKey) String() string { func (*TupleKey) ProtoMessage() {} func (x *TupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[0] + mi := &file_extention_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -61,7 +830,7 @@ func (x *TupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKey.ProtoReflect.Descriptor instead. func (*TupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{0} + return file_extention_proto_rawDescGZIP(), []int{12} } func (x *TupleKey) GetUser() string { @@ -102,7 +871,7 @@ type Tuple struct { func (x *Tuple) Reset() { *x = Tuple{} - mi := &file_extention_proto_msgTypes[1] + mi := &file_extention_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -114,7 +883,7 @@ func (x *Tuple) String() string { func (*Tuple) ProtoMessage() {} func (x *Tuple) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[1] + mi := &file_extention_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -127,7 +896,7 @@ func (x *Tuple) ProtoReflect() protoreflect.Message { // Deprecated: Use Tuple.ProtoReflect.Descriptor instead. func (*Tuple) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{1} + return file_extention_proto_rawDescGZIP(), []int{13} } func (x *Tuple) GetKey() *TupleKey { @@ -155,7 +924,7 @@ type TupleKeyWithoutCondition struct { func (x *TupleKeyWithoutCondition) Reset() { *x = TupleKeyWithoutCondition{} - mi := &file_extention_proto_msgTypes[2] + mi := &file_extention_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -167,7 +936,7 @@ func (x *TupleKeyWithoutCondition) String() string { func (*TupleKeyWithoutCondition) ProtoMessage() {} func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[2] + mi := &file_extention_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -180,7 +949,7 @@ func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKeyWithoutCondition.ProtoReflect.Descriptor instead. func (*TupleKeyWithoutCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{2} + return file_extention_proto_rawDescGZIP(), []int{14} } func (x *TupleKeyWithoutCondition) GetUser() string { @@ -214,7 +983,7 @@ type RelationshipCondition struct { func (x *RelationshipCondition) Reset() { *x = RelationshipCondition{} - mi := &file_extention_proto_msgTypes[3] + mi := &file_extention_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -226,7 +995,7 @@ func (x *RelationshipCondition) String() string { func (*RelationshipCondition) ProtoMessage() {} func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[3] + mi := &file_extention_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -239,7 +1008,7 @@ func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use RelationshipCondition.ProtoReflect.Descriptor instead. func (*RelationshipCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{3} + return file_extention_proto_rawDescGZIP(), []int{15} } func (x *RelationshipCondition) GetName() string { @@ -268,7 +1037,7 @@ type ReadRequest struct { func (x *ReadRequest) Reset() { *x = ReadRequest{} - mi := &file_extention_proto_msgTypes[4] + mi := &file_extention_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -280,7 +1049,7 @@ func (x *ReadRequest) String() string { func (*ReadRequest) ProtoMessage() {} func (x *ReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[4] + mi := &file_extention_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -293,7 +1062,7 @@ func (x *ReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. func (*ReadRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{4} + return file_extention_proto_rawDescGZIP(), []int{16} } func (x *ReadRequest) GetNamespace() string { @@ -335,7 +1104,7 @@ type ReadRequestTupleKey struct { func (x *ReadRequestTupleKey) Reset() { *x = ReadRequestTupleKey{} - mi := &file_extention_proto_msgTypes[5] + mi := &file_extention_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -347,7 +1116,7 @@ func (x *ReadRequestTupleKey) String() string { func (*ReadRequestTupleKey) ProtoMessage() {} func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[5] + mi := &file_extention_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -360,7 +1129,7 @@ func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequestTupleKey.ProtoReflect.Descriptor instead. func (*ReadRequestTupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{5} + return file_extention_proto_rawDescGZIP(), []int{17} } func (x *ReadRequestTupleKey) GetUser() string { @@ -394,7 +1163,7 @@ type ReadResponse struct { func (x *ReadResponse) Reset() { *x = ReadResponse{} - mi := &file_extention_proto_msgTypes[6] + mi := &file_extention_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -406,7 +1175,7 @@ func (x *ReadResponse) String() string { func (*ReadResponse) ProtoMessage() {} func (x *ReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[6] + mi := &file_extention_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -419,7 +1188,7 @@ func (x *ReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. func (*ReadResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{6} + return file_extention_proto_rawDescGZIP(), []int{18} } func (x *ReadResponse) GetTuples() []*Tuple { @@ -445,7 +1214,7 @@ type WriteRequestWrites struct { func (x *WriteRequestWrites) Reset() { *x = WriteRequestWrites{} - mi := &file_extention_proto_msgTypes[7] + mi := &file_extention_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -457,7 +1226,7 @@ func (x *WriteRequestWrites) String() string { func (*WriteRequestWrites) ProtoMessage() {} func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[7] + mi := &file_extention_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -470,7 +1239,7 @@ func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestWrites.ProtoReflect.Descriptor instead. func (*WriteRequestWrites) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{7} + return file_extention_proto_rawDescGZIP(), []int{19} } func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey { @@ -489,7 +1258,7 @@ type WriteRequestDeletes struct { func (x *WriteRequestDeletes) Reset() { *x = WriteRequestDeletes{} - mi := &file_extention_proto_msgTypes[8] + mi := &file_extention_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -501,7 +1270,7 @@ func (x *WriteRequestDeletes) String() string { func (*WriteRequestDeletes) ProtoMessage() {} func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[8] + mi := &file_extention_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -514,7 +1283,7 @@ func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestDeletes.ProtoReflect.Descriptor instead. func (*WriteRequestDeletes) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{8} + return file_extention_proto_rawDescGZIP(), []int{20} } func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition { @@ -535,7 +1304,7 @@ type WriteRequest struct { func (x *WriteRequest) Reset() { *x = WriteRequest{} - mi := &file_extention_proto_msgTypes[9] + mi := &file_extention_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -547,7 +1316,7 @@ func (x *WriteRequest) String() string { func (*WriteRequest) ProtoMessage() {} func (x *WriteRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[9] + mi := &file_extention_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -560,7 +1329,7 @@ func (x *WriteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead. func (*WriteRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{9} + return file_extention_proto_rawDescGZIP(), []int{21} } func (x *WriteRequest) GetNamespace() string { @@ -592,7 +1361,7 @@ type WriteResponse struct { func (x *WriteResponse) Reset() { *x = WriteResponse{} - mi := &file_extention_proto_msgTypes[10] + mi := &file_extention_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -604,7 +1373,7 @@ func (x *WriteResponse) String() string { func (*WriteResponse) ProtoMessage() {} func (x *WriteResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[10] + mi := &file_extention_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -617,7 +1386,7 @@ func (x *WriteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteResponse.ProtoReflect.Descriptor instead. func (*WriteResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{10} + return file_extention_proto_rawDescGZIP(), []int{22} } type BatchCheckRequest struct { @@ -631,7 +1400,7 @@ type BatchCheckRequest struct { func (x *BatchCheckRequest) Reset() { *x = BatchCheckRequest{} - mi := &file_extention_proto_msgTypes[11] + mi := &file_extention_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -643,7 +1412,7 @@ func (x *BatchCheckRequest) String() string { func (*BatchCheckRequest) ProtoMessage() {} func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[11] + mi := &file_extention_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -656,7 +1425,7 @@ func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead. func (*BatchCheckRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{11} + return file_extention_proto_rawDescGZIP(), []int{23} } func (x *BatchCheckRequest) GetSubject() string { @@ -694,7 +1463,7 @@ type BatchCheckItem struct { func (x *BatchCheckItem) Reset() { *x = BatchCheckItem{} - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -706,7 +1475,7 @@ func (x *BatchCheckItem) String() string { func (*BatchCheckItem) ProtoMessage() {} func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -719,7 +1488,7 @@ func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead. func (*BatchCheckItem) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{12} + return file_extention_proto_rawDescGZIP(), []int{24} } func (x *BatchCheckItem) GetVerb() string { @@ -773,7 +1542,7 @@ type BatchCheckResponse struct { func (x *BatchCheckResponse) Reset() { *x = BatchCheckResponse{} - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -785,7 +1554,7 @@ func (x *BatchCheckResponse) String() string { func (*BatchCheckResponse) ProtoMessage() {} func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -798,7 +1567,7 @@ func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead. func (*BatchCheckResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{13} + return file_extention_proto_rawDescGZIP(), []int{25} } func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource { @@ -817,7 +1586,7 @@ type BatchCheckGroupResource struct { func (x *BatchCheckGroupResource) Reset() { *x = BatchCheckGroupResource{} - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -829,7 +1598,7 @@ func (x *BatchCheckGroupResource) String() string { func (*BatchCheckGroupResource) ProtoMessage() {} func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -842,7 +1611,7 @@ func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckGroupResource.ProtoReflect.Descriptor instead. func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{14} + return file_extention_proto_rawDescGZIP(), []int{26} } func (x *BatchCheckGroupResource) GetItems() map[string]bool { @@ -852,6 +1621,280 @@ func (x *BatchCheckGroupResource) GetItems() map[string]bool { return nil } +type QueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Operation *QueryOperation `protobuf:"bytes,2,opt,name=operation,proto3" json:"operation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryRequest) Reset() { + *x = QueryRequest{} + mi := &file_extention_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRequest) ProtoMessage() {} + +func (x *QueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. +func (*QueryRequest) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{27} +} + +func (x *QueryRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *QueryRequest) GetOperation() *QueryOperation { + if x != nil { + return x.Operation + } + return nil +} + +type QueryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Result: + // + // *QueryResponse_FolderParents + Result isQueryResponse_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryResponse) Reset() { + *x = QueryResponse{} + mi := &file_extention_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryResponse) ProtoMessage() {} + +func (x *QueryResponse) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead. +func (*QueryResponse) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{28} +} + +func (x *QueryResponse) GetResult() isQueryResponse_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *QueryResponse) GetFolderParents() *GetFolderParentsResult { + if x != nil { + if x, ok := x.Result.(*QueryResponse_FolderParents); ok { + return x.FolderParents + } + } + return nil +} + +type isQueryResponse_Result interface { + isQueryResponse_Result() +} + +type QueryResponse_FolderParents struct { + FolderParents *GetFolderParentsResult `protobuf:"bytes,1,opt,name=folder_parents,json=folderParents,proto3,oneof"` +} + +func (*QueryResponse_FolderParents) isQueryResponse_Result() {} + +type QueryOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Operation: + // + // *QueryOperation_GetFolderParents + Operation isQueryOperation_Operation `protobuf_oneof:"operation"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryOperation) Reset() { + *x = QueryOperation{} + mi := &file_extention_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryOperation) ProtoMessage() {} + +func (x *QueryOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryOperation.ProtoReflect.Descriptor instead. +func (*QueryOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{29} +} + +func (x *QueryOperation) GetOperation() isQueryOperation_Operation { + if x != nil { + return x.Operation + } + return nil +} + +func (x *QueryOperation) GetGetFolderParents() *GetFolderParentsQuery { + if x != nil { + if x, ok := x.Operation.(*QueryOperation_GetFolderParents); ok { + return x.GetFolderParents + } + } + return nil +} + +type isQueryOperation_Operation interface { + isQueryOperation_Operation() +} + +type QueryOperation_GetFolderParents struct { + GetFolderParents *GetFolderParentsQuery `protobuf:"bytes,1,opt,name=get_folder_parents,json=getFolderParents,proto3,oneof"` +} + +func (*QueryOperation_GetFolderParents) isQueryOperation_Operation() {} + +type GetFolderParentsQuery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UID of the folder + Folder string `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFolderParentsQuery) Reset() { + *x = GetFolderParentsQuery{} + mi := &file_extention_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFolderParentsQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFolderParentsQuery) ProtoMessage() {} + +func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFolderParentsQuery.ProtoReflect.Descriptor instead. +func (*GetFolderParentsQuery) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{30} +} + +func (x *GetFolderParentsQuery) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +type GetFolderParentsResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // List of parent folder UIDs + ParentUids []string `protobuf:"bytes,1,rep,name=parent_uids,json=parentUids,proto3" json:"parent_uids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFolderParentsResult) Reset() { + *x = GetFolderParentsResult{} + mi := &file_extention_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFolderParentsResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFolderParentsResult) ProtoMessage() {} + +func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFolderParentsResult.ProtoReflect.Descriptor instead. +func (*GetFolderParentsResult) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{31} +} + +func (x *GetFolderParentsResult) GetParentUids() []string { + if x != nil { + return x.ParentUids + } + return nil +} + var File_extention_proto protoreflect.FileDescriptor var file_extention_proto_rawDesc = string([]byte{ @@ -863,150 +1906,295 @@ var file_extention_proto_rawDesc = string([]byte{ 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, - 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x63, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x72, 0x0a, 0x0d, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, + 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4d, 0x75, 0x74, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa8, 0x05, 0x0a, 0x0f, 0x4d, + 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, + 0x0a, 0x11, 0x73, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x65, 0x74, 0x46, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x50, 0x0a, 0x0d, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0c, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x5c, 0x0a, 0x11, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x10, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x5c, 0x0a, 0x11, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x61, 0x0a, 0x14, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6f, 0x72, 0x67, 0x5f, 0x72, 0x6f, 0x6c, 0x65, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x61, 0x0a, 0x14, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6f, 0x72, 0x67, 0x5f, 0x72, + 0x6f, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x58, + 0x0a, 0x11, 0x61, 0x64, 0x64, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6f, 0x72, 0x67, 0x5f, 0x72, + 0x6f, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x41, + 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x55, 0x73, 0x65, + 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, + 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x70, 0x0a, 0x15, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, + 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x95, 0x01, 0x0a, + 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, - 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, - 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, 0x0a, 0x15, 0x52, 0x65, 0x6c, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, 0x01, 0x0a, 0x0b, 0x52, 0x65, - 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x38, 0x0a, - 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x70, - 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x69, - 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, + 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x0a, 0x17, + 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, + 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, + 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, + 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, - 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, - 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, - 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, 0x57, 0x72, 0x69, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x3b, 0x0a, - 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, - 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x62, 0x0a, 0x13, 0x57, 0x72, - 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, - 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0xaf, - 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x3e, 0x0a, - 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x50, 0x0a, 0x08, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x48, 0x0a, + 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, + 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, 0x70, 0x6c, + 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, 0x0a, 0x09, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, + 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x2e, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, + 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, 0x0a, 0x15, + 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, 0x01, 0x0a, + 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x74, 0x75, + 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, - 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x41, 0x0a, - 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, - 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, - 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, - 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, 0x01, 0x0a, 0x0e, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, - 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, - 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, - 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x67, 0x72, 0x6f, - 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa1, 0x01, 0x0a, 0x17, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, + 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, + 0x12, 0x38, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, + 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, 0x65, 0x61, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, + 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, 0x70, 0x6c, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, + 0x70, 0x6c, 0x65, 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x63, + 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, 0x57, 0x72, + 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, + 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, + 0x65, 0x79, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x62, 0x0a, + 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, + 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, + 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x12, 0x3e, 0x0a, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, + 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, 0x01, 0x0a, + 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, + 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, + 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, 0x62, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, + 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, - 0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, - 0x8d, 0x02, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa1, + 0x01, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, + 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, 0x65, 0x6d, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x6e, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x12, 0x40, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, + 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x22, 0x78, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, + 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x48, 0x00, 0x52, 0x10, 0x67, + 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, + 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x15, + 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x39, 0x0a, + 0x16, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x5f, 0x75, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, + 0x72, 0x65, 0x6e, 0x74, 0x55, 0x69, 0x64, 0x73, 0x32, 0xac, 0x03, 0x0a, 0x15, 0x41, 0x75, 0x74, + 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, - 0x38, 0x5a, 0x36, 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, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, + 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 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, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -1021,56 +2209,92 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 34) var file_extention_proto_goTypes = []any{ - (*TupleKey)(nil), // 0: authz.extention.v1.TupleKey - (*Tuple)(nil), // 1: authz.extention.v1.Tuple - (*TupleKeyWithoutCondition)(nil), // 2: authz.extention.v1.TupleKeyWithoutCondition - (*RelationshipCondition)(nil), // 3: authz.extention.v1.RelationshipCondition - (*ReadRequest)(nil), // 4: authz.extention.v1.ReadRequest - (*ReadRequestTupleKey)(nil), // 5: authz.extention.v1.ReadRequestTupleKey - (*ReadResponse)(nil), // 6: authz.extention.v1.ReadResponse - (*WriteRequestWrites)(nil), // 7: authz.extention.v1.WriteRequestWrites - (*WriteRequestDeletes)(nil), // 8: authz.extention.v1.WriteRequestDeletes - (*WriteRequest)(nil), // 9: authz.extention.v1.WriteRequest - (*WriteResponse)(nil), // 10: authz.extention.v1.WriteResponse - (*BatchCheckRequest)(nil), // 11: authz.extention.v1.BatchCheckRequest - (*BatchCheckItem)(nil), // 12: authz.extention.v1.BatchCheckItem - (*BatchCheckResponse)(nil), // 13: authz.extention.v1.BatchCheckResponse - (*BatchCheckGroupResource)(nil), // 14: authz.extention.v1.BatchCheckGroupResource - nil, // 15: authz.extention.v1.BatchCheckResponse.GroupsEntry - nil, // 16: authz.extention.v1.BatchCheckGroupResource.ItemsEntry - (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 18: google.protobuf.Struct - (*wrapperspb.Int32Value)(nil), // 19: google.protobuf.Int32Value + (*MutateRequest)(nil), // 0: authz.extention.v1.MutateRequest + (*MutateResponse)(nil), // 1: authz.extention.v1.MutateResponse + (*MutateOperation)(nil), // 2: authz.extention.v1.MutateOperation + (*SetFolderParentOperation)(nil), // 3: authz.extention.v1.SetFolderParentOperation + (*DeleteFolderOperation)(nil), // 4: authz.extention.v1.DeleteFolderOperation + (*CreatePermissionOperation)(nil), // 5: authz.extention.v1.CreatePermissionOperation + (*DeletePermissionOperation)(nil), // 6: authz.extention.v1.DeletePermissionOperation + (*AddUserOrgRoleOperation)(nil), // 7: authz.extention.v1.AddUserOrgRoleOperation + (*UpdateUserOrgRoleOperation)(nil), // 8: authz.extention.v1.UpdateUserOrgRoleOperation + (*DeleteUserOrgRoleOperation)(nil), // 9: authz.extention.v1.DeleteUserOrgRoleOperation + (*Resource)(nil), // 10: authz.extention.v1.Resource + (*Permission)(nil), // 11: authz.extention.v1.Permission + (*TupleKey)(nil), // 12: authz.extention.v1.TupleKey + (*Tuple)(nil), // 13: authz.extention.v1.Tuple + (*TupleKeyWithoutCondition)(nil), // 14: authz.extention.v1.TupleKeyWithoutCondition + (*RelationshipCondition)(nil), // 15: authz.extention.v1.RelationshipCondition + (*ReadRequest)(nil), // 16: authz.extention.v1.ReadRequest + (*ReadRequestTupleKey)(nil), // 17: authz.extention.v1.ReadRequestTupleKey + (*ReadResponse)(nil), // 18: authz.extention.v1.ReadResponse + (*WriteRequestWrites)(nil), // 19: authz.extention.v1.WriteRequestWrites + (*WriteRequestDeletes)(nil), // 20: authz.extention.v1.WriteRequestDeletes + (*WriteRequest)(nil), // 21: authz.extention.v1.WriteRequest + (*WriteResponse)(nil), // 22: authz.extention.v1.WriteResponse + (*BatchCheckRequest)(nil), // 23: authz.extention.v1.BatchCheckRequest + (*BatchCheckItem)(nil), // 24: authz.extention.v1.BatchCheckItem + (*BatchCheckResponse)(nil), // 25: authz.extention.v1.BatchCheckResponse + (*BatchCheckGroupResource)(nil), // 26: authz.extention.v1.BatchCheckGroupResource + (*QueryRequest)(nil), // 27: authz.extention.v1.QueryRequest + (*QueryResponse)(nil), // 28: authz.extention.v1.QueryResponse + (*QueryOperation)(nil), // 29: authz.extention.v1.QueryOperation + (*GetFolderParentsQuery)(nil), // 30: authz.extention.v1.GetFolderParentsQuery + (*GetFolderParentsResult)(nil), // 31: authz.extention.v1.GetFolderParentsResult + nil, // 32: authz.extention.v1.BatchCheckResponse.GroupsEntry + nil, // 33: authz.extention.v1.BatchCheckGroupResource.ItemsEntry + (*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 35: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 36: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ - 3, // 0: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition - 0, // 1: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey - 17, // 2: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp - 18, // 3: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct - 5, // 4: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey - 19, // 5: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value - 1, // 6: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple - 0, // 7: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey - 2, // 8: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition - 7, // 9: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites - 8, // 10: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes - 12, // 11: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem - 15, // 12: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry - 16, // 13: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry - 14, // 14: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource - 11, // 15: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest - 4, // 16: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest - 9, // 17: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest - 13, // 18: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse - 6, // 19: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse - 10, // 20: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse - 18, // [18:21] is the sub-list for method output_type - 15, // [15:18] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 2, // 0: authz.extention.v1.MutateRequest.operations:type_name -> authz.extention.v1.MutateOperation + 3, // 1: authz.extention.v1.MutateOperation.set_folder_parent:type_name -> authz.extention.v1.SetFolderParentOperation + 4, // 2: authz.extention.v1.MutateOperation.delete_folder:type_name -> authz.extention.v1.DeleteFolderOperation + 5, // 3: authz.extention.v1.MutateOperation.create_permission:type_name -> authz.extention.v1.CreatePermissionOperation + 6, // 4: authz.extention.v1.MutateOperation.delete_permission:type_name -> authz.extention.v1.DeletePermissionOperation + 8, // 5: authz.extention.v1.MutateOperation.update_user_org_role:type_name -> authz.extention.v1.UpdateUserOrgRoleOperation + 9, // 6: authz.extention.v1.MutateOperation.delete_user_org_role:type_name -> authz.extention.v1.DeleteUserOrgRoleOperation + 7, // 7: authz.extention.v1.MutateOperation.add_user_org_role:type_name -> authz.extention.v1.AddUserOrgRoleOperation + 10, // 8: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 11, // 9: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 10, // 10: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 11, // 11: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 15, // 12: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition + 12, // 13: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey + 34, // 14: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 35, // 15: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 17, // 16: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey + 36, // 17: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 13, // 18: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple + 12, // 19: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey + 14, // 20: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition + 19, // 21: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites + 20, // 22: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes + 24, // 23: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem + 32, // 24: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry + 33, // 25: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry + 29, // 26: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation + 31, // 27: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult + 30, // 28: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery + 26, // 29: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource + 23, // 30: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest + 16, // 31: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 21, // 32: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 0, // 33: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest + 27, // 34: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest + 25, // 35: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse + 18, // 36: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 22, // 37: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 1, // 38: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse + 28, // 39: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse + 35, // [35:40] is the sub-list for method output_type + 30, // [30:35] 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_extention_proto_init() } @@ -1078,13 +2302,28 @@ func file_extention_proto_init() { if File_extention_proto != nil { return } + file_extention_proto_msgTypes[2].OneofWrappers = []any{ + (*MutateOperation_SetFolderParent)(nil), + (*MutateOperation_DeleteFolder)(nil), + (*MutateOperation_CreatePermission)(nil), + (*MutateOperation_DeletePermission)(nil), + (*MutateOperation_UpdateUserOrgRole)(nil), + (*MutateOperation_DeleteUserOrgRole)(nil), + (*MutateOperation_AddUserOrgRole)(nil), + } + file_extention_proto_msgTypes[28].OneofWrappers = []any{ + (*QueryResponse_FolderParents)(nil), + } + file_extention_proto_msgTypes[29].OneofWrappers = []any{ + (*QueryOperation_GetFolderParents)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_extention_proto_rawDesc), len(file_extention_proto_rawDesc)), NumEnums: 0, - NumMessages: 17, + NumMessages: 34, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index 9d64c718b56..2a11b8c5b97 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -13,6 +13,96 @@ service AuthzExtentionService { rpc Read(ReadRequest) returns (ReadResponse); rpc Write(WriteRequest) returns (WriteResponse); + + rpc Mutate(MutateRequest) returns (MutateResponse); + rpc Query(QueryRequest) returns (QueryResponse); +} + +message MutateRequest { + string namespace = 1; + repeated MutateOperation operations = 2; +} + +message MutateResponse {} + +message MutateOperation { + oneof operation { + SetFolderParentOperation set_folder_parent = 1; + DeleteFolderOperation delete_folder = 2; + CreatePermissionOperation create_permission = 3; + DeletePermissionOperation delete_permission = 4; + UpdateUserOrgRoleOperation update_user_org_role = 5; + DeleteUserOrgRoleOperation delete_user_org_role = 6; + AddUserOrgRoleOperation add_user_org_role = 7; + } +} + +message SetFolderParentOperation { + // UID of the folder + string folder = 1; + // UID of the parent folder + string parent = 2; + // If true, delete all existing parent relations associated with the folder + bool delete_existing = 3; +} + +message DeleteFolderOperation { + // UID of the folder to delete + string folder = 1; + // UID of the parent folder + string parent = 2; + // If true, delete all existing parent relations associated with the folder + bool delete_existing = 3; +} + +message CreatePermissionOperation { + Resource resource = 1; + Permission permission = 2; +} + +message DeletePermissionOperation { + Resource resource = 1; + Permission permission = 2; +} + +message AddUserOrgRoleOperation { + // User UID + string user = 1; + // Role name (e.g: "Admin", "Editor", "Viewer") + string role = 2; +} + +// UpdateUserOrgRoleOperation assigns the user's basic role and deletes existing basic role assignments. +message UpdateUserOrgRoleOperation { + // User UID + string user = 1; + // Role name (e.g: "Admin", "Editor", "Viewer") + string role = 2; +} + +message DeleteUserOrgRoleOperation { + // User UID + string user = 1; + // Role name (e.g: "Admin", "Editor", "Viewer") + string role = 2; +} + +message Resource { + // group of the resource (e.g: "dashboard.grafana.app") + string group = 1; + // kind of the resource (e.g: "dashboards") + string resource = 2; + // uid of the resource + string name = 3; +} + +message Permission { + // kind of the identity getting the permission (e.g: "user", "team", "serviceaccount") + string kind = 1; + // uid of the identity getting the permission + string name = 2; + // action set granted to the user (e.g. "admin" or "edit", "view") + string verb = 3; } message TupleKey { @@ -94,3 +184,30 @@ message BatchCheckResponse { message BatchCheckGroupResource { map items = 1; } + +message QueryRequest { + string namespace = 1; + QueryOperation operation = 2; +} + +message QueryResponse { + oneof result { + GetFolderParentsResult folder_parents = 1; + } +} + +message QueryOperation { + oneof operation { + GetFolderParentsQuery get_folder_parents = 1; + } +} + +message GetFolderParentsQuery { + // UID of the folder + string folder = 1; +} + +message GetFolderParentsResult { + // List of parent folder UIDs + repeated string parent_uids = 1; +} diff --git a/pkg/services/authz/proto/v1/extention_grpc.pb.go b/pkg/services/authz/proto/v1/extention_grpc.pb.go index 78eb6e30b58..b320dbe8c07 100644 --- a/pkg/services/authz/proto/v1/extention_grpc.pb.go +++ b/pkg/services/authz/proto/v1/extention_grpc.pb.go @@ -22,6 +22,8 @@ const ( AuthzExtentionService_BatchCheck_FullMethodName = "/authz.extention.v1.AuthzExtentionService/BatchCheck" AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read" AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write" + AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate" + AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query" ) // AuthzExtentionServiceClient is the client API for AuthzExtentionService service. @@ -31,6 +33,8 @@ type AuthzExtentionServiceClient interface { BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error) + Mutate(ctx context.Context, in *MutateRequest, opts ...grpc.CallOption) (*MutateResponse, error) + Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryResponse, error) } type authzExtentionServiceClient struct { @@ -71,6 +75,26 @@ func (c *authzExtentionServiceClient) Write(ctx context.Context, in *WriteReques return out, nil } +func (c *authzExtentionServiceClient) Mutate(ctx context.Context, in *MutateRequest, opts ...grpc.CallOption) (*MutateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MutateResponse) + err := c.cc.Invoke(ctx, AuthzExtentionService_Mutate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authzExtentionServiceClient) Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QueryResponse) + err := c.cc.Invoke(ctx, AuthzExtentionService_Query_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AuthzExtentionServiceServer is the server API for AuthzExtentionService service. // All implementations should embed UnimplementedAuthzExtentionServiceServer // for forward compatibility @@ -78,6 +102,8 @@ type AuthzExtentionServiceServer interface { BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) Read(context.Context, *ReadRequest) (*ReadResponse, error) Write(context.Context, *WriteRequest) (*WriteResponse, error) + Mutate(context.Context, *MutateRequest) (*MutateResponse, error) + Query(context.Context, *QueryRequest) (*QueryResponse, error) } // UnimplementedAuthzExtentionServiceServer should be embedded to have forward compatible implementations. @@ -93,6 +119,12 @@ func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadReque func (UnimplementedAuthzExtentionServiceServer) Write(context.Context, *WriteRequest) (*WriteResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Write not implemented") } +func (UnimplementedAuthzExtentionServiceServer) Mutate(context.Context, *MutateRequest) (*MutateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Mutate not implemented") +} +func (UnimplementedAuthzExtentionServiceServer) Query(context.Context, *QueryRequest) (*QueryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Query not implemented") +} // UnsafeAuthzExtentionServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to AuthzExtentionServiceServer will @@ -159,6 +191,42 @@ func _AuthzExtentionService_Write_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _AuthzExtentionService_Mutate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthzExtentionServiceServer).Mutate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthzExtentionService_Mutate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthzExtentionServiceServer).Mutate(ctx, req.(*MutateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthzExtentionService_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthzExtentionServiceServer).Query(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthzExtentionService_Query_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthzExtentionServiceServer).Query(ctx, req.(*QueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AuthzExtentionService_ServiceDesc is the grpc.ServiceDesc for AuthzExtentionService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -178,6 +246,14 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Write", Handler: _AuthzExtentionService_Write_Handler, }, + { + MethodName: "Mutate", + Handler: _AuthzExtentionService_Mutate_Handler, + }, + { + MethodName: "Query", + Handler: _AuthzExtentionService_Query_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "extention.proto", diff --git a/pkg/services/authz/rbac.go b/pkg/services/authz/rbac.go index ed58f7ed686..23de10511a2 100644 --- a/pkg/services/authz/rbac.go +++ b/pkg/services/authz/rbac.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/services/authz/rbac" "github.com/grafana/grafana/pkg/services/authz/rbac/store" "github.com/grafana/grafana/pkg/services/authz/zanzana" + zClient "github.com/grafana/grafana/pkg/services/authz/zanzana/client" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/setting" @@ -84,7 +85,7 @@ func ProvideAuthZClient( case clientModeCloud: rbacClient, err := newRemoteRBACClient(authCfg, tracer, reg) if zanzanaEnabled { - return zanzana.WithShadowClient(rbacClient, zanzanaClient, reg) + return zClient.WithShadowClient(rbacClient, zanzanaClient, reg) } return rbacClient, err default: @@ -131,7 +132,7 @@ func ProvideAuthZClient( ) if zanzanaEnabled { - return zanzana.WithShadowClient(rbacClient, zanzanaClient, reg) + return zClient.WithShadowClient(rbacClient, zanzanaClient, reg) } return rbacClient, nil @@ -148,12 +149,34 @@ func ProvideStandaloneAuthZClient( return nil, nil } + //nolint:staticcheck // not yet migrated to OpenFeature + zanzanaEnabled := features.IsEnabledGlobally(featuremgmt.FlagZanzana) + + zanzanaClient, err := ProvideStandaloneZanzanaClient(cfg, features) + if err != nil { + return nil, err + } + + //nolint:staticcheck // not yet migrated to OpenFeature + if zanzanaEnabled && features.IsEnabledGlobally(featuremgmt.FlagZanzanaNoLegacyClient) { + return zanzanaClient, nil + } + authCfg, err := readAuthzClientSettings(cfg) if err != nil { return nil, err } - return newRemoteRBACClient(authCfg, tracer, reg) + remoteRBACClient, err := newRemoteRBACClient(authCfg, tracer, reg) + if err != nil { + return nil, err + } + + if zanzanaEnabled { + return zClient.WithShadowClient(remoteRBACClient, zanzanaClient, reg) + } + + return remoteRBACClient, nil } func newRemoteRBACClient(clientCfg *authzClientSettings, tracer trace.Tracer, reg prometheus.Registerer) (authlib.AccessClient, error) { diff --git a/pkg/services/authz/rbac/mapper.go b/pkg/services/authz/rbac/mapper.go index 5ef0a252ed8..fd7e5d2140c 100644 --- a/pkg/services/authz/rbac/mapper.go +++ b/pkg/services/authz/rbac/mapper.go @@ -24,17 +24,18 @@ type Mapping interface { AllActions() []string // HasFolderSupport returns true if the translation supports folders. HasFolderSupport() bool - // SkipScopeOnCreate returns true if the translation does not require a scope on create. - SkipScopeOnCreate() bool + // SkipScope returns true if the translation does not require a scope for the given verb. + SkipScope(verb string) bool } type translation struct { - resource string - attribute string - verbMapping map[string]string - actionSetMapping map[string][]string - folderSupport bool - skipScopeOnCreate bool + resource string + attribute string + verbMapping map[string]string + actionSetMapping map[string][]string + folderSupport bool + // actions to skip scope on, e.g., create actions + skipScopeOnVerb map[string]bool // use this option if you need to limit access to users that can access all resources useWildcardScope bool } @@ -77,8 +78,11 @@ func (t translation) HasFolderSupport() bool { return t.folderSupport } -func (t translation) SkipScopeOnCreate() bool { - return t.skipScopeOnCreate +func (t translation) SkipScope(verb string) bool { + if t.skipScopeOnVerb != nil { + return t.skipScopeOnVerb[verb] + } + return false } // MapperRegistry is a registry of mappers that maps a group and resource to a translation. @@ -92,7 +96,7 @@ type MapperRegistry interface { type mapper map[string]map[string]translation -func newResourceTranslation(resource string, attribute string, folderSupport, skipScopeOnCreate bool) translation { +func newResourceTranslation(resource string, attribute string, folderSupport bool, skipScopeOnVerb map[string]bool) translation { defaultMapping := func(r string) map[string]string { return map[string]string{ utils.VerbGet: fmt.Sprintf("%s:read", r), @@ -109,17 +113,17 @@ func newResourceTranslation(resource string, attribute string, folderSupport, sk } return translation{ - resource: resource, - attribute: attribute, - verbMapping: defaultMapping(resource), - folderSupport: folderSupport, - skipScopeOnCreate: skipScopeOnCreate, + resource: resource, + attribute: attribute, + verbMapping: defaultMapping(resource), + folderSupport: folderSupport, + skipScopeOnVerb: skipScopeOnVerb, } } // newDashboardTranslation creates a translation for dashboards and also maps the actions to action sets func newDashboardTranslation() translation { - dashTranslation := newResourceTranslation("dashboards", "uid", true, false) + dashTranslation := newResourceTranslation("dashboards", "uid", true, nil) actionSetMapping := make(map[string][]string) for verb, rbacAction := range dashTranslation.verbMapping { @@ -152,7 +156,7 @@ func newDashboardTranslation() translation { // newFolderTranslation creates a translation for folders and also maps the actions to action sets func newFolderTranslation() translation { - folderTranslation := newResourceTranslation("folders", "uid", true, false) + folderTranslation := newResourceTranslation("folders", "uid", true, nil) actionSetMapping := make(map[string][]string) for verb, rbacAction := range folderTranslation.verbMapping { @@ -178,22 +182,55 @@ func newFolderTranslation() translation { return folderTranslation } +func newExternalGroupMappingTranslation() translation { + return translation{ + resource: "teams.permissions", + attribute: "uid", + verbMapping: map[string]string{ + utils.VerbGet: "teams.permissions:read", + utils.VerbList: "teams.permissions:read", + utils.VerbWatch: "teams.permissions:read", + utils.VerbCreate: "teams.permissions:write", + utils.VerbUpdate: "teams.permissions:write", + utils.VerbPatch: "teams.permissions:write", + utils.VerbDelete: "teams.permissions:write", + utils.VerbGetPermissions: "teams.permissions:write", + utils.VerbSetPermissions: "teams.permissions:write", + }, + folderSupport: false, + } +} + func NewMapperRegistry() MapperRegistry { + skipScopeOnAllVerbs := map[string]bool{ + utils.VerbCreate: true, + utils.VerbGet: true, + utils.VerbUpdate: true, + utils.VerbPatch: true, + utils.VerbDelete: true, + utils.VerbDeleteCollection: true, + utils.VerbList: true, + utils.VerbWatch: true, + utils.VerbGetPermissions: true, + utils.VerbSetPermissions: true, + } + mapper := mapper(map[string]map[string]translation{ "dashboard.grafana.app": { "dashboards": newDashboardTranslation(), - "librarypanels": newResourceTranslation("library.panels", "uid", true, false), + "librarypanels": newResourceTranslation("library.panels", "uid", true, nil), }, "folder.grafana.app": { "folders": newFolderTranslation(), }, "iam.grafana.app": { // Users is a special case. We translate user permissions from id to uid based. - "users": newResourceTranslation("users", "uid", false, true), - "serviceaccounts": newResourceTranslation("serviceaccounts", "uid", false, true), + "users": newResourceTranslation("users", "uid", false, map[string]bool{utils.VerbCreate: true}), + "serviceaccounts": newResourceTranslation("serviceaccounts", "uid", false, map[string]bool{utils.VerbCreate: true}), // Teams is a special case. We translate user permissions from id to uid based. - "teams": newResourceTranslation("teams", "uid", false, true), - // No need to skip scope on create for roles because we translate `permissions:type:delegate` to `roles:*`` + "teams": newResourceTranslation("teams", "uid", false, map[string]bool{utils.VerbCreate: true}), + // ExternalGroupMappings is a special case. We translate team permissions from id to uid based. + "externalgroupmappings": newExternalGroupMappingTranslation(), "coreroles": translation{ resource: "roles", attribute: "uid", @@ -202,8 +239,9 @@ func NewMapperRegistry() MapperRegistry { utils.VerbList: "roles:read", utils.VerbWatch: "roles:read", }, - folderSupport: false, - skipScopeOnCreate: false, + folderSupport: false, + // No need to skip scope on create for roles because we translate `permissions:type:delegate` to `roles:*`` + skipScopeOnVerb: nil, }, "roles": translation{ resource: "roles", @@ -218,8 +256,9 @@ func NewMapperRegistry() MapperRegistry { utils.VerbList: "roles:read", utils.VerbWatch: "roles:read", }, - folderSupport: false, - skipScopeOnCreate: false, + folderSupport: false, + // No need to skip scope on create for roles because we translate `permissions:type:delegate` to `roles:*`` + skipScopeOnVerb: nil, }, "rolebindings": translation{ resource: "rolebindings", @@ -238,9 +277,14 @@ func NewMapperRegistry() MapperRegistry { folderSupport: false, }, }, + "provisioning.grafana.app": { + "repositories": newResourceTranslation("provisioning.repositories", "uid", false, skipScopeOnAllVerbs), + "jobs": newResourceTranslation("provisioning.jobs", "uid", false, skipScopeOnAllVerbs), + "historicjobs": newResourceTranslation("provisioning.historicjobs", "uid", false, skipScopeOnAllVerbs), + }, "secret.grafana.app": { - "securevalues": newResourceTranslation("secret.securevalues", "uid", false, false), - "keepers": newResourceTranslation("secret.keepers", "uid", false, false), + "securevalues": newResourceTranslation("secret.securevalues", "uid", false, nil), + "keepers": newResourceTranslation("secret.keepers", "uid", false, nil), }, "query.grafana.app": { "query": translation{ @@ -249,8 +293,8 @@ func NewMapperRegistry() MapperRegistry { verbMapping: map[string]string{ utils.VerbCreate: "datasources:query", }, - folderSupport: false, - skipScopeOnCreate: false, + folderSupport: false, + skipScopeOnVerb: nil, }, }, }) diff --git a/pkg/services/authz/rbac/resolver.go b/pkg/services/authz/rbac/resolver.go index 56aacf539d6..0077d17c8ad 100644 --- a/pkg/services/authz/rbac/resolver.go +++ b/pkg/services/authz/rbac/resolver.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/registry/apis/iam/common" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" ) @@ -54,7 +55,7 @@ func (s *Service) newServiceAccountNameResolver(ctx context.Context, ns types.Na func (s *Service) fetchTeams(ctx context.Context, ns types.NamespaceInfo) (map[int64]string, error) { key := teamIDsCacheKey(ns.Value) res, err, _ := s.sf.Do(key, func() (any, error) { - teams, err := s.identityStore.ListTeams(ctx, ns, legacy.ListTeamQuery{}) + teams, err := s.identityStore.ListTeams(ctx, ns, legacy.ListTeamQuery{Pagination: common.Pagination{Limit: 100}}) if err != nil { return nil, fmt.Errorf("could not fetch teams: %w", err) } @@ -170,6 +171,7 @@ func (s *Service) nameResolver(ctx context.Context, ns types.NamespaceInfo, scop if scopePrefix == "teams:id:" { return s.newTeamNameResolver(ctx, ns) } + if scopePrefix == "permissions:type:" { return permissionsDelegateResolverFunc, nil } diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 6ba8e0a8838..e45346740cc 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -617,18 +617,28 @@ func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, return false, status.Error(codes.NotFound, "unsupported resource") } - if req.Verb == utils.VerbCreate { - // Resource doesn't require scope on create, so allow if the user has the action - if t.SkipScopeOnCreate() { - return scopeMap[""], nil - } - // If creating a resource that goes in a folder, but no folder is specified, - // assume parent folder is the general folder - if t.HasFolderSupport() && req.ParentFolder == "" { - req.ParentFolder = accesscontrol.GeneralFolderUID - } + if t.SkipScope(req.Verb) { + return scopeMap[""], nil } + // If creating a resource that goes in a folder, but no folder is specified, + // assume parent folder is the general folder + if req.Verb == utils.VerbCreate && t.HasFolderSupport() && req.ParentFolder == "" { + req.ParentFolder = accesscontrol.GeneralFolderUID + } + + //if req.Verb == utils.VerbCreate { + // // Resource doesn't require scope on create, so allow if the user has the action + // if t.SkipScopeOnCreate() { + // return scopeMap[""], nil + // } + // // If creating a resource that goes in a folder, but no folder is specified, + // // assume parent folder is the general folder + // if t.HasFolderSupport() && req.ParentFolder == "" { + // req.ParentFolder = accesscontrol.GeneralFolderUID + // } + //} + // Wildcard grant, no further checks needed if scopeMap["*"] { return true, nil diff --git a/pkg/services/authz/wireset.go b/pkg/services/authz/wireset.go index 4a46f15ea3b..6540413325b 100644 --- a/pkg/services/authz/wireset.go +++ b/pkg/services/authz/wireset.go @@ -6,5 +6,5 @@ import ( var WireSet = wire.NewSet( ProvideAuthZClient, - ProvideZanzana, + ProvideZanzanaClient, ) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 36abf4779c1..3be1b18dc1d 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -24,26 +24,26 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana" + zClient "github.com/grafana/grafana/pkg/services/authz/zanzana/client" + zServer "github.com/grafana/grafana/pkg/services/authz/zanzana/server" + zStore "github.com/grafana/grafana/pkg/services/authz/zanzana/store" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/setting" ) -// ProvideZanzana used to register ZanzanaClient. +// ProvideZanzanaClient used to register ZanzanaClient. // It will also start an embedded ZanzanaSever if mode is set to "embedded". -func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (zanzana.Client, error) { +func ProvideZanzanaClient(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features featuremgmt.FeatureToggles, reg prometheus.Registerer) (zanzana.Client, error) { //nolint:staticcheck // not yet migrated to OpenFeature if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) { - return zanzana.NewNoopClient(), nil + return zClient.NewNoopClient(), nil } - logger := log.New("zanzana.server") - - var client zanzana.Client switch cfg.ZanzanaClient.Mode { case setting.ZanzanaModeClient: - return NewZanzanaClient( + return NewRemoteZanzanaClient( fmt.Sprintf("stacks-%s", cfg.StackID), ZanzanaClientConfig{ URL: cfg.ZanzanaClient.Addr, @@ -51,18 +51,20 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL, ServerCertFile: cfg.ZanzanaClient.ServerCertFile, }) + case setting.ZanzanaModeEmbedded: - store, err := zanzana.NewEmbeddedStore(cfg, db, logger) + logger := log.New("zanzana.server") + store, err := zStore.NewEmbeddedStore(cfg, db, logger) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } - openfga, err := zanzana.NewOpenFGAServer(cfg.ZanzanaServer, store) + openfga, err := zServer.NewOpenFGAServer(cfg.ZanzanaServer, store) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } - srv, err := zanzana.NewServer(cfg.ZanzanaServer, openfga, logger, tracer, reg) + srv, err := zServer.NewServer(cfg.ZanzanaServer, openfga, logger, tracer, reg) if err != nil { return nil, fmt.Errorf("failed to start zanzana: %w", err) } @@ -82,16 +84,33 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features authzv1.RegisterAuthzServiceServer(channel, srv) authzextv1.RegisterAuthzExtentionServiceServer(channel, srv) - client, err = zanzana.NewClient(channel) + client, err := zClient.New(channel) if err != nil { return nil, fmt.Errorf("failed to initialize zanzana client: %w", err) } + return client, nil default: return nil, fmt.Errorf("unsupported zanzana mode: %s", cfg.ZanzanaClient.Mode) } +} - return client, nil +// ProvideStandaloneZanzanaClient provides a standalone Zanzana client, without registering the Zanzana service. +// Client connects to a remote Zanzana server specified in the configuration. +func ProvideStandaloneZanzanaClient(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (zanzana.Client, error) { + //nolint:staticcheck // not yet migrated to OpenFeature + if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) { + return zClient.NewNoopClient(), nil + } + + zanzanaConfig := ZanzanaClientConfig{ + URL: cfg.ZanzanaClient.Addr, + Token: cfg.ZanzanaClient.Token, + TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL, + ServerCertFile: cfg.ZanzanaClient.ServerCertFile, + } + + return NewRemoteZanzanaClient(cfg.ZanzanaClient.TokenNamespace, zanzanaConfig) } type ZanzanaClientConfig struct { @@ -101,7 +120,8 @@ type ZanzanaClientConfig struct { ServerCertFile string } -func NewZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client, error) { +// NewRemoteZanzanaClient creates a new Zanzana client that connects to remote Zanzana server. +func NewRemoteZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client, error) { tokenClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{ Token: cfg.Token, TokenExchangeURL: cfg.TokenExchangeURL, @@ -134,7 +154,7 @@ func NewZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err) } - client, err := zanzana.NewClient(conn) + client, err := zClient.New(conn) if err != nil { return nil, fmt.Errorf("failed to initialize zanzana client: %w", err) } @@ -186,17 +206,17 @@ func (z *Zanzana) start(ctx context.Context) error { return err } - store, err := zanzana.NewStore(z.cfg, z.logger) + store, err := zStore.NewStore(z.cfg, z.logger) if err != nil { return fmt.Errorf("failed to initilize zanana store: %w", err) } - openfgaServer, err := zanzana.NewOpenFGAServer(z.cfg.ZanzanaServer, store) + openfgaServer, err := zServer.NewOpenFGAServer(z.cfg.ZanzanaServer, store) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } - zanzanaServer, err := zanzana.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger, tracer, z.reg) + zanzanaServer, err := zServer.NewServer(z.cfg.ZanzanaServer, openfgaServer, z.logger, tracer, z.reg) if err != nil { return fmt.Errorf("failed to start zanzana: %w", err) } @@ -240,7 +260,7 @@ func (z *Zanzana) start(ctx context.Context) error { authzextv1.RegisterAuthzExtentionServiceServer(grpcServer, zanzanaServer) // register grpc health server - healthServer := zanzana.NewHealthServer(zanzanaServer) + healthServer := zServer.NewHealthServer(zanzanaServer) healthv1pb.RegisterHealthServer(grpcServer, healthServer) if _, err := grpcserver.ProvideReflectionService(z.cfg, z.handle); err != nil { @@ -253,7 +273,7 @@ func (z *Zanzana) start(ctx context.Context) error { func (z *Zanzana) running(ctx context.Context) error { if z.cfg.Env == setting.Dev && z.cfg.ZanzanaServer.OpenFGAHttpAddr != "" { go func() { - srv, err := zanzana.NewOpenFGAHttpServer(z.cfg.ZanzanaServer, z.handle) + srv, err := zServer.NewOpenFGAHttpServer(z.cfg.ZanzanaServer, z.handle) if err != nil { z.logger.Error("failed to create OpenFGA HTTP server", "error", err) } else { diff --git a/pkg/services/authz/zanzana/client.go b/pkg/services/authz/zanzana/client.go index 40b672bf759..f238fd27159 100644 --- a/pkg/services/authz/zanzana/client.go +++ b/pkg/services/authz/zanzana/client.go @@ -3,13 +3,9 @@ package zanzana import ( "context" - "google.golang.org/grpc" - authlib "github.com/grafana/authlib/types" - "github.com/prometheus/client_golang/prometheus" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" - "github.com/grafana/grafana/pkg/services/authz/zanzana/client" ) // Client is a wrapper around [openfgav1.OpenFGAServiceClient] @@ -18,16 +14,7 @@ type Client interface { Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) Write(ctx context.Context, req *authzextv1.WriteRequest) error BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) -} -func NewClient(cc grpc.ClientConnInterface) (*client.Client, error) { - return client.New(cc) -} - -func WithShadowClient(accessClient authlib.AccessClient, zanzanaClient authlib.AccessClient, reg prometheus.Registerer) (authlib.AccessClient, error) { - return client.WithShadowClient(accessClient, zanzanaClient, reg), nil -} - -func NewNoopClient() *client.NoopClient { - return client.NewNoop() + Mutate(ctx context.Context, req *authzextv1.MutateRequest) error + Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) } diff --git a/pkg/services/authz/zanzana/client/client.go b/pkg/services/authz/zanzana/client/client.go index 05266afba83..3c51d707561 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -12,9 +12,11 @@ import ( "github.com/grafana/grafana/pkg/infra/log" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" ) var _ authlib.AccessClient = (*Client)(nil) +var _ zanzana.Client = (*Client)(nil) var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/client") @@ -72,3 +74,26 @@ func (c *Client) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckReque return c.authzext.BatchCheck(ctx, req) } + +func (c *Client) WriteNew(ctx context.Context, req *authzextv1.WriteRequest) error { + ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Write") + defer span.End() + + _, err := c.authzext.Write(ctx, req) + return err +} + +func (c *Client) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { + ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Mutate") + defer span.End() + + _, err := c.authzext.Mutate(ctx, req) + return err +} + +func (c *Client) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Query") + defer span.End() + + return c.authzext.Query(ctx, req) +} diff --git a/pkg/services/authz/zanzana/client/noop.go b/pkg/services/authz/zanzana/client/noop.go index 419a9b73201..73860cd0c4e 100644 --- a/pkg/services/authz/zanzana/client/noop.go +++ b/pkg/services/authz/zanzana/client/noop.go @@ -6,11 +6,13 @@ import ( authlib "github.com/grafana/authlib/types" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" ) var _ authlib.AccessClient = (*NoopClient)(nil) +var _ zanzana.Client = (*NoopClient)(nil) -func NewNoop() *NoopClient { +func NewNoopClient() *NoopClient { return &NoopClient{} } @@ -35,3 +37,11 @@ func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) er func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { return nil, nil } + +func (nc NoopClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error { + return nil +} + +func (nc NoopClient) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + return nil, nil +} diff --git a/pkg/services/authz/zanzana/client/shadow_client.go b/pkg/services/authz/zanzana/client/shadow_client.go index 4fb4c545ff0..f2f0ec7d4f1 100644 --- a/pkg/services/authz/zanzana/client/shadow_client.go +++ b/pkg/services/authz/zanzana/client/shadow_client.go @@ -6,6 +6,7 @@ import ( "github.com/prometheus/client_golang/prometheus" authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/log" ) @@ -19,14 +20,14 @@ type ShadowClient struct { } // WithShadowClient returns a new access client that runs zanzana checks in the background. -func WithShadowClient(accessClient authlib.AccessClient, zanzanaClient authlib.AccessClient, reg prometheus.Registerer) authlib.AccessClient { +func WithShadowClient(accessClient authlib.AccessClient, zanzanaClient authlib.AccessClient, reg prometheus.Registerer) (authlib.AccessClient, error) { client := &ShadowClient{ logger: log.New("zanzana-shadow-client"), accessClient: accessClient, zanzanaClient: zanzanaClient, metrics: newShadowClientMetrics(reg), } - return client + return client, nil } func (c *ShadowClient) Check(ctx context.Context, id authlib.AuthInfo, req authlib.CheckRequest, folder string) (authlib.CheckResponse, error) { diff --git a/pkg/services/authz/zanzana/translations.go b/pkg/services/authz/zanzana/common/translations.go similarity index 54% rename from pkg/services/authz/zanzana/translations.go rename to pkg/services/authz/zanzana/common/translations.go index 0abefcf2e29..25f3c22318b 100644 --- a/pkg/services/authz/zanzana/translations.go +++ b/pkg/services/authz/zanzana/common/translations.go @@ -1,6 +1,10 @@ -package zanzana +package common import ( + "slices" + + authlib "github.com/grafana/authlib/types" + dashboards "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" ) @@ -21,6 +25,14 @@ var basicRolesTranslations = map[string]string{ roleNone: "basic_none", } +var basicRolesUIDs = []string{ + "basic_grafana_admin", + "basic_admin", + "basic_editor", + "basic_viewer", + "basic_none", +} + type resourceTranslation struct { typ string group string @@ -65,6 +77,13 @@ var resourceTranslations = map[string]resourceTranslation{ "dashboards:write": newScopedMapping(RelationUpdate, dashboardGroup, dashboardResource, ""), "dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource, ""), "dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource, ""), + // Action sets + "folders:view": newMapping(RelationSetView, ""), + "folders:edit": newMapping(RelationSetEdit, ""), + "folders:admin": newMapping(RelationSetAdmin, ""), + "dashboards:view": newScopedMapping(RelationSetView, dashboardGroup, dashboardResource, ""), + "dashboards:edit": newScopedMapping(RelationSetEdit, dashboardGroup, dashboardResource, ""), + "dashboards:admin": newScopedMapping(RelationSetAdmin, dashboardGroup, dashboardResource, ""), }, }, KindDashboards: { @@ -76,6 +95,71 @@ var resourceTranslations = map[string]resourceTranslation{ "dashboards:write": newMapping(RelationUpdate, ""), "dashboards:create": newMapping(RelationCreate, ""), "dashboards:delete": newMapping(RelationDelete, ""), + // Action sets + "dashboards:view": newMapping(RelationSetView, ""), + "dashboards:edit": newMapping(RelationSetEdit, ""), + "dashboards:admin": newMapping(RelationSetAdmin, ""), }, }, } + +func TranslateToCheckRequest(namespace, action, kind, name string) (*authlib.CheckRequest, bool) { + translation, ok := resourceTranslations[kind] + + if !ok { + return nil, false + } + + m, ok := translation.mapping[action] + if !ok { + return nil, false + } + + verb, ok := RelationToVerbMapping[m.relation] + if !ok { + return nil, false + } + + req := &authlib.CheckRequest{ + Namespace: namespace, + Verb: verb, + Group: translation.group, + Resource: translation.resource, + Name: name, + } + + return req, true +} + +func TranslateToListRequest(namespace, action, kind string) (*authlib.ListRequest, bool) { + translation, ok := resourceTranslations[kind] + + if !ok { + return nil, false + } + + // FIXME: support different verbs + req := &authlib.ListRequest{ + Namespace: namespace, + Group: translation.group, + Resource: translation.resource, + } + + return req, true +} + +func TranslateToGroupResource(kind string) string { + translation, ok := resourceTranslations[kind] + if !ok { + return "" + } + return FormatGroupResource(translation.group, translation.resource, "") +} + +func TranslateBasicRole(name string) string { + return basicRolesTranslations[name] +} + +func IsBasicRole(name string) bool { + return slices.Contains(basicRolesUIDs, name) +} diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go index 41ed7c2a85b..79efce7373d 100644 --- a/pkg/services/authz/zanzana/common/tuple.go +++ b/pkg/services/authz/zanzana/common/tuple.go @@ -1,12 +1,14 @@ package common import ( + "fmt" "strings" openfgav1 "github.com/openfga/api/proto/openfga/v1" "google.golang.org/protobuf/types/known/structpb" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) @@ -33,6 +35,11 @@ const ( TypeTeamPrefix string = TypeTeam + ":" ) +const ( + KindDashboards string = dashboardV1.DASHBOARD_RESOURCE + KindFolders string = folderV1.RESOURCE +) + const ( RelationTeamMember string = "member" RelationTeamAdmin string = "admin" @@ -144,6 +151,10 @@ func isValidRelation(relation string, valid []string) bool { return false } +func IsFolderResourceTuple(t *openfgav1.TupleKey) bool { + return strings.HasPrefix(t.Object, TypeFolder) && strings.HasPrefix(t.Relation, "resource_") +} + func SubresourceRelation(relation string) string { return TypeResource + "_" + relation } @@ -178,6 +189,69 @@ func FormatGroupResource(group, resource, subresource string) string { return b.String() } +// NewTupleEntry constructs new openfga entry type:name[#relation]. +// Relation allows to specify group of users (subjects) related to type:name +// (for example, team:devs#member refers to users which are members of team devs) +func NewTupleEntry(objectType, name, relation string) string { + obj := fmt.Sprintf("%s:%s", objectType, name) + if relation != "" { + obj = fmt.Sprintf("%s#%s", obj, relation) + } + return obj +} + +func NewObjectEntry(objectType, group, resource, subresource, name string) string { + if objectType == TypeFolder { + return TypeFolder + ":" + name + } + + obj := fmt.Sprintf("%s:%s/%s", objectType, group, resource) + if subresource != "" { + obj = fmt.Sprintf("%s/%s", obj, subresource) + } + if name != "" { + obj = fmt.Sprintf("%s/%s", obj, name) + } + return obj +} + +func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) { + translation, ok := resourceTranslations[kind] + + if !ok { + return nil, false + } + + m, ok := translation.mapping[action] + if !ok { + return nil, false + } + + if name == "*" { + return NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource), true + } + + if translation.typ == TypeResource { + return NewResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource, name), true + } + + if translation.typ == TypeFolder { + if m.group != "" && m.resource != "" { + return NewFolderResourceTuple(subject, m.relation, m.group, m.resource, m.subresource, name), true + } + + return NewFolderTuple(subject, m.relation, name), true + } + + return NewTypedTuple(translation.typ, subject, m.relation, name), true +} + +func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) { + va := a.Condition.Context.Fields["subresources"] + vb := b.Condition.Context.Fields["subresources"] + va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...) +} + func NewResourceTuple(subject, relation, group, resource, subresource, name string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ User: subject, @@ -200,6 +274,18 @@ func isSubresourceRelationSet(relation string) bool { relation == RelationSubresourceSetAdmin } +func NewFolderParentTuple(folder, parent string) *openfgav1.TupleKey { + return &openfgav1.TupleKey{ + Object: NewFolderIdent(folder), + Relation: RelationParent, + User: NewFolderIdent(parent), + } +} + +func NewFolderTuple(subject, relation, name string) *openfgav1.TupleKey { + return NewTypedTuple(TypeFolder, subject, relation, name) +} + func NewFolderResourceTuple(subject, relation, group, resource, subresource, folder string) *openfgav1.TupleKey { relation = SubresourceRelation(relation) var condition *openfgav1.RelationshipCondition @@ -256,18 +342,6 @@ func NewGroupResourceTuple(subject, relation, group, resource, subresource strin } } -func NewFolderParentTuple(folder, parent string) *openfgav1.TupleKey { - return &openfgav1.TupleKey{ - Object: NewFolderIdent(folder), - Relation: RelationParent, - User: NewFolderIdent(parent), - } -} - -func NewFolderTuple(subject, relation, name string) *openfgav1.TupleKey { - return NewTypedTuple(TypeFolder, subject, relation, name) -} - func NewTypedTuple(typ, subject, relation, name string) *openfgav1.TupleKey { return &openfgav1.TupleKey{ User: subject, @@ -276,6 +350,14 @@ func NewTypedTuple(typ, subject, relation, name string) *openfgav1.TupleKey { } } +func NewTuple(subject, relation, object string) *openfgav1.TupleKey { + return &openfgav1.TupleKey{ + User: subject, + Relation: relation, + Object: object, + } +} + func ToAuthzExtTupleKey(t *openfgav1.TupleKey) *authzextv1.TupleKey { tupleKey := &authzextv1.TupleKey{ User: t.GetUser(), @@ -383,3 +465,21 @@ func AddRenderContext(req *openfgav1.CheckRequest) { ), }) } + +func SplitTupleObject(object string) (string, string, string) { + var objectType, name, relation string + parts := strings.Split(object, ":") + if len(parts) < 2 { + return "", "", "" + } + + objectType = parts[0] + nameRel := parts[1] + parts = strings.Split(nameRel, "#") + if len(parts) > 1 { + relation = parts[1] + } + name = parts[0] + + return objectType, name, relation +} diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go index 91a9800efbd..3505f0dcec1 100644 --- a/pkg/services/authz/zanzana/server.go +++ b/pkg/services/authz/zanzana/server.go @@ -1,31 +1 @@ package zanzana - -import ( - "net/http" - - openfgaserver "github.com/openfga/openfga/pkg/server" - openfgastorage "github.com/openfga/openfga/pkg/storage" - "github.com/prometheus/client_golang/prometheus" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/authz/zanzana/server" - "github.com/grafana/grafana/pkg/services/grpcserver" - "github.com/grafana/grafana/pkg/setting" -) - -func NewServer(cfg setting.ZanzanaServerSettings, openfga server.OpenFGAServer, logger log.Logger, tracer tracing.Tracer, reg prometheus.Registerer) (*server.Server, error) { - return server.NewServer(cfg, openfga, logger, tracer, reg) -} - -func NewHealthServer(target server.DiagnosticServer) *server.HealthServer { - return server.NewHealthServer(target) -} - -func NewOpenFGAServer(cfg setting.ZanzanaServerSettings, store openfgastorage.OpenFGADatastore) (*openfgaserver.Server, error) { - return server.NewOpenFGAServer(cfg, store) -} - -func NewOpenFGAHttpServer(cfg setting.ZanzanaServerSettings, srv grpcserver.Provider) (*http.Server, error) { - return server.NewOpenFGAHttpServer(cfg, srv) -} diff --git a/pkg/services/authz/zanzana/server/auth.go b/pkg/services/authz/zanzana/server/auth.go index e86aaa81ebb..f137bdca47f 100644 --- a/pkg/services/authz/zanzana/server/auth.go +++ b/pkg/services/authz/zanzana/server/auth.go @@ -26,7 +26,7 @@ func authorize(ctx context.Context, namespace string, ss setting.ZanzanaServerSe return status.Errorf(codes.Unauthenticated, "unauthenticated") } if !claims.NamespaceMatches(c.GetNamespace(), namespace) { - return status.Errorf(codes.PermissionDenied, "namespace does not match") + return status.Errorf(codes.PermissionDenied, "token namespace %s does not match request namespace", c.GetNamespace()) } return nil } diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go new file mode 100644 index 00000000000..3a691ed6e12 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -0,0 +1,97 @@ +package server + +import ( + "context" + "errors" + "fmt" + "time" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +type OperationGroup string + +const ( + OperationGroupFolder OperationGroup = "folder" + OperationGroupPermission OperationGroup = "permission" + OperationGroupUserOrgRole OperationGroup = "user_org_role" +) + +func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { + ctx, span := s.tracer.Start(ctx, "server.Mutate") + defer span.End() + + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.Mutate", req.GetNamespace()).Observe(time.Since(t).Seconds()) + }(time.Now()) + + res, err := s.mutate(ctx, req) + if err != nil { + s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace()) + return nil, errors.New("failed to perform mutate request") + } + + return res, nil +} + +func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { + if err := authorize(ctx, req.GetNamespace(), s.cfg); err != nil { + return nil, err + } + + storeInf, err := s.getStoreInfo(ctx, req.Namespace) + if err != nil { + return nil, fmt.Errorf("failed to get openfga store: %w", err) + } + + groupedOperations, err := groupByOperation(req.GetOperations()) + if err != nil { + return nil, fmt.Errorf("failed to group operations: %w", err) + } + + for operationGroup, operations := range groupedOperations { + switch operationGroup { + case OperationGroupFolder: + if err := s.mutateFolders(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate folder: %w", err) + } + case OperationGroupPermission: + if err := s.mutateResourcePermissions(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate resource permissions: %w", err) + } + case OperationGroupUserOrgRole: + if err := s.mutateOrgRoles(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate org roles: %w", err) + } + default: + s.logger.Warn("unsupported operation group", "operationGroup", operationGroup) + } + } + + return &authzextv1.MutateResponse{}, nil +} + +func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, error) { + switch operation.Operation.(type) { + case *authzextv1.MutateOperation_SetFolderParent, *authzextv1.MutateOperation_DeleteFolder: + return OperationGroupFolder, nil + case *authzextv1.MutateOperation_CreatePermission, *authzextv1.MutateOperation_DeletePermission: + return OperationGroupPermission, nil + case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole, *authzextv1.MutateOperation_AddUserOrgRole: + return OperationGroupUserOrgRole, nil + } + return OperationGroup(""), errors.New("unsupported mutate operation type") +} + +func groupByOperation(operations []*authzextv1.MutateOperation) (map[OperationGroup][]*authzextv1.MutateOperation, error) { + grouped := make(map[OperationGroup][]*authzextv1.MutateOperation) + for _, operation := range operations { + operationGroup, err := getOperationGroup(operation) + if err != nil { + return nil, err + } + grouped[operationGroup] = append(grouped[operationGroup], operation) + } + + return grouped, nil +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_folder.go b/pkg/services/authz/zanzana/server/server_mutate_folder.go new file mode 100644 index 00000000000..3d92347f404 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_folder.go @@ -0,0 +1,142 @@ +package server + +import ( + "context" + "fmt" + "strings" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func (s *Server) mutateFolders(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateFolder") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_SetFolderParent: + tuple, err := s.getFolderWriteTuple(ctx, store, op.SetFolderParent) + if err != nil { + return err + } + if tuple != nil { + writeTuples = append(writeTuples, tuple) + } + + // Delete existing parent tuples + if op.SetFolderParent.GetDeleteExisting() { + tuples, err := s.getFolderDeleteTuples(ctx, store, op.SetFolderParent.GetFolder(), op.SetFolderParent.GetParent(), true) + if err != nil { + return err + } + deleteTuples = append(deleteTuples, tuples...) + } + case *authzextv1.MutateOperation_DeleteFolder: + tuples, err := s.getFolderDeleteTuples(ctx, store, op.DeleteFolder.GetFolder(), op.DeleteFolder.GetParent(), op.DeleteFolder.GetDeleteExisting()) + if err != nil { + return err + } + deleteTuples = append(deleteTuples, tuples...) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + if len(writeTuples) == 0 && len(deleteTuples) == 0 { + return nil + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write folder tuples", "error", err) + return err + } + + return nil +} + +func (s *Server) getFolderWriteTuple(ctx context.Context, store *storeInfo, req *authzextv1.SetFolderParentOperation) (*openfgav1.TupleKey, error) { + // Folder is at the root level + if req.GetParent() == "" { + return nil, nil + } + + if strings.ContainsAny(req.GetFolder(), "#:") { + return nil, fmt.Errorf("folder UID contains invalid characters: %s", req.GetFolder()) + } + + tuple := zanzana.NewFolderParentTuple(req.GetFolder(), req.GetParent()) + return tuple, nil +} + +func (s *Server) getFolderDeleteTuples(ctx context.Context, store *storeInfo, folderUID string, parentUID string, deleteExisting bool) ([]*openfgav1.TupleKeyWithoutCondition, error) { + tupleKeysToDelete := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + if folderUID != "" && parentUID != "" && !deleteExisting { + tuple := zanzana.NewFolderParentTuple(folderUID, parentUID) + tupleKeysToDelete = append(tupleKeysToDelete, &openfgav1.TupleKeyWithoutCondition{ + User: tuple.GetUser(), + Relation: tuple.GetRelation(), + Object: tuple.GetObject(), + }) + } + + if deleteExisting { + parentTuples, err := s.listFolderParents(ctx, store, folderUID) + if err != nil { + return nil, fmt.Errorf("failed to list folder parents: %w", err) + } + + for _, tuple := range parentTuples { + tupleKeysToDelete = append(tupleKeysToDelete, &openfgav1.TupleKeyWithoutCondition{ + User: tuple.Key.User, + Relation: tuple.Key.Relation, + Object: tuple.Key.Object, + }) + } + } + + return tupleKeysToDelete, nil +} + +func (s *Server) listFolderParents(ctx context.Context, store *storeInfo, folderUID string) ([]*openfgav1.Tuple, error) { + ctx, span := s.tracer.Start(ctx, "server.listFolderParents") + defer span.End() + + object := zanzana.NewFolderIdent(folderUID) + resp, err := s.openfga.Read(ctx, &openfgav1.ReadRequest{ + StoreId: store.ID, + TupleKey: &openfgav1.ReadRequestTupleKey{ + Object: object, + Relation: zanzana.RelationParent, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to list folder parents: %w", err) + } + + return resp.Tuples, nil +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_folder_test.go b/pkg/services/authz/zanzana/server/server_mutate_folder_test.go new file mode 100644 index 00000000000..a01c3b08740 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_folder_test.go @@ -0,0 +1,164 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateFolders(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewFolderParentTuple("11", "1"), + common.NewFolderParentTuple("12", "1"), + common.NewFolderParentTuple("111", "11"), + common.NewFolderParentTuple("112", "11"), + common.NewFolderParentTuple("broken", "foo"), + common.NewFolderParentTuple("broken", "bar"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateFolders(t *testing.T, srv *Server) { + setupMutateFolders(t, srv) + + t.Run("should create new folder parent relation", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_SetFolderParent{ + SetFolderParent: &v1.SetFolderParentOperation{ + Folder: "new-folder", + Parent: "1", + DeleteExisting: false, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:new-folder", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "folder:new-folder", res.Tuples[0].Key.Object) + require.Equal(t, "parent", res.Tuples[0].Key.Relation) + require.Equal(t, "folder:1", res.Tuples[0].Key.User) + }) + + t.Run("should delete folder parent relation", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeleteFolder{ + DeleteFolder: &v1.DeleteFolderOperation{ + Folder: "11", + Parent: "1", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:11", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) + + t.Run("should clean up all parent relations", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeleteFolder{ + DeleteFolder: &v1.DeleteFolderOperation{ + Folder: "broken", + DeleteExisting: true, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:broken", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) + + t.Run("should perform batch mutate if multiple operations are provided", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_SetFolderParent{ + SetFolderParent: &v1.SetFolderParentOperation{ + Folder: "new-folder-2", + Parent: "1", + }, + }, + }, + { + Operation: &v1.MutateOperation_DeleteFolder{ + DeleteFolder: &v1.DeleteFolderOperation{ + Folder: "12", + Parent: "1", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:new-folder-2", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "folder:new-folder-2", res.Tuples[0].Key.Object) + require.Equal(t, "parent", res.Tuples[0].Key.Relation) + require.Equal(t, "folder:1", res.Tuples[0].Key.User) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:12", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_org_role.go b/pkg/services/authz/zanzana/server/server_mutate_org_role.go new file mode 100644 index 00000000000..bda9decb3d5 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_org_role.go @@ -0,0 +1,113 @@ +package server + +import ( + "context" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func (s *Server) mutateOrgRoles(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateOrgRoles") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_AddUserOrgRole: + basicRole := zanzana.TranslateBasicRole(op.AddUserOrgRole.GetRole()) + tuple := &openfgav1.TupleKey{ + User: zanzana.NewTupleEntry(zanzana.TypeUser, op.AddUserOrgRole.GetUser(), ""), + Relation: zanzana.RelationAssignee, + Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""), + } + writeTuples = append(writeTuples, tuple) + case *authzextv1.MutateOperation_DeleteUserOrgRole: + basicRole := zanzana.TranslateBasicRole(op.DeleteUserOrgRole.GetRole()) + tuple := &openfgav1.TupleKeyWithoutCondition{ + User: zanzana.NewTupleEntry(zanzana.TypeUser, op.DeleteUserOrgRole.GetUser(), ""), + Relation: zanzana.RelationAssignee, + Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""), + } + deleteTuples = append(deleteTuples, tuple) + case *authzextv1.MutateOperation_UpdateUserOrgRole: + writeTuple, existingTuples, err := s.getUserOrgRoleUpdateTuples(ctx, store, op.UpdateUserOrgRole) + if err != nil { + return err + } + writeTuples = append(writeTuples, writeTuple) + deleteTuples = append(deleteTuples, existingTuples...) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + if len(writeTuples) == 0 && len(deleteTuples) == 0 { + return nil + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write user org role tuples", "error", err) + return err + } + + return nil +} + +func (s *Server) getUserOrgRoleUpdateTuples(ctx context.Context, store *storeInfo, req *authzextv1.UpdateUserOrgRoleOperation) (*openfgav1.TupleKey, []*openfgav1.TupleKeyWithoutCondition, error) { + readReq := &openfgav1.ReadRequest{ + StoreId: store.ID, + TupleKey: &openfgav1.ReadRequestTupleKey{ + User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""), + Relation: zanzana.RelationAssignee, + // read tuples by object type ("role:") + Object: zanzana.NewTupleEntry(zanzana.TypeRole, "", ""), + }, + } + res, err := s.openfga.Read(ctx, readReq) + if err != nil { + return nil, nil, err + } + existingBasicRoleTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + for _, tuple := range res.GetTuples() { + _, roleName, _ := zanzana.SplitTupleObject(tuple.GetKey().GetObject()) + if zanzana.IsBasicRole(roleName) { + existingBasicRoleTuples = append(existingBasicRoleTuples, &openfgav1.TupleKeyWithoutCondition{ + User: tuple.GetKey().GetUser(), + Relation: tuple.GetKey().GetRelation(), + Object: tuple.GetKey().GetObject(), + }) + } + } + + basicRole := zanzana.TranslateBasicRole(req.GetRole()) + writeTuple := &openfgav1.TupleKey{ + User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""), + Relation: zanzana.RelationAssignee, + Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""), + } + + return writeTuple, existingBasicRoleTuples, nil +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_org_role_test.go b/pkg/services/authz/zanzana/server/server_mutate_org_role_test.go new file mode 100644 index 00000000000..7c7472cf6ac --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_org_role_test.go @@ -0,0 +1,110 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateOrgRoles(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewTuple("user:1", common.RelationAssignee, "role:basic_editor"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateOrgRoles(t *testing.T, srv *Server) { + setupMutateOrgRoles(t, srv) + + t.Run("should update user org role and delete old role", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_UpdateUserOrgRole{ + UpdateUserOrgRole: &v1.UpdateUserOrgRoleOperation{ + User: "1", + Role: "Admin", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:basic_admin", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:1", res.Tuples[0].Key.User) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:basic_editor", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) + + t.Run("should add user org role and delete old role", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_AddUserOrgRole{ + AddUserOrgRole: &v1.AddUserOrgRoleOperation{ + User: "1", + Role: "Viewer", + }, + }, + }, + { + Operation: &v1.MutateOperation_DeleteUserOrgRole{ + DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{ + User: "1", + Role: "Admin", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:basic_admin", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:basic_viewer", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:1", res.Tuples[0].Key.User) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go new file mode 100644 index 00000000000..fa8b5467235 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go @@ -0,0 +1,173 @@ +package server + +import ( + "context" + "errors" + "fmt" + "strings" + + "google.golang.org/protobuf/types/known/structpb" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +var ( + errEmptyName = errors.New("name cannot be empty") + errInvalidBasicRole = errors.New("invalid basic role") + errUnknownKind = errors.New("unknown permission kind") +) + +func (s *Server) mutateResourcePermissions(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateResourcePermissions") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_CreatePermission: + tuple, err := s.getPermissionWriteTuple(ctx, op.CreatePermission) + if err != nil { + return err + } + writeTuples = append(writeTuples, tuple) + case *authzextv1.MutateOperation_DeletePermission: + tuple, err := s.getPermissionDeleteTuple(ctx, op.DeletePermission) + if err != nil { + return err + } + deleteTuples = append(deleteTuples, tuple) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write resource permission tuples", "error", err) + return err + } + + return nil +} + +func (s *Server) getPermissionWriteTuple(ctx context.Context, req *authzextv1.CreatePermissionOperation) (*openfgav1.TupleKey, error) { + resource := req.GetResource() + permission := req.GetPermission() + object := zanzana.NewObjectEntry(toZanzanaType(resource.GetGroup()), resource.GetGroup(), resource.GetResource(), "", resource.GetName()) + tuple, err := NewResourceTuple(object, resource, permission) + if err != nil { + return nil, err + } + + return tuple, nil +} + +func (s *Server) getPermissionDeleteTuple(ctx context.Context, req *authzextv1.DeletePermissionOperation) (*openfgav1.TupleKeyWithoutCondition, error) { + resource := req.GetResource() + permission := req.GetPermission() + object := zanzana.NewObjectEntry(toZanzanaType(resource.GetGroup()), resource.GetGroup(), resource.GetResource(), "", resource.GetName()) + tuple, err := NewResourceTuple(object, resource, permission) + if err != nil { + return nil, err + } + + return &openfgav1.TupleKeyWithoutCondition{ + User: tuple.GetUser(), + Relation: tuple.GetRelation(), + Object: tuple.GetObject(), + }, nil +} + +func toZanzanaType(apiGroup string) string { + if apiGroup == "folder.grafana.app" { + return zanzana.TypeFolder + } + return zanzana.TypeResource +} + +func NewResourceTuple(object string, resource *authzextv1.Resource, perm *authzextv1.Permission) (*openfgav1.TupleKey, error) { + // Typ is "folder" or "resource" + typ := toZanzanaType(resource.Group) + + // subject + subject, err := toZanzanaSubject(perm.GetKind(), perm.GetName()) + if err != nil { + return nil, err + } + + key := &openfgav1.TupleKey{ + // e.g. "user:{uid}", "serviceaccount:{uid}", "team:{uid}", "basicrole:{viewer|editor|admin}" + User: subject, + // "view", "edit", "admin" + Relation: strings.ToLower(perm.Verb), + // e.g. "folder:{name}" or "resource:{apiGroup}/{resource}/{name}" + Object: object, + } + + // For resources we add a condition to filter by apiGroup/resource + // e.g "group_filter": {"group_resource": "dashboards.grafana.app/dashboards"} + if typ == zanzana.TypeResource { + key.Condition = &openfgav1.RelationshipCondition{ + Name: "group_filter", + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "group_resource": structpb.NewStringValue( + resource.GetGroup() + "/" + resource.GetResource(), + ), + }, + }, + } + } + + return key, nil +} + +func toZanzanaSubject(kind string, name string) (string, error) { + if name == "" { + return "", errEmptyName + } + iamKind := iamv0.ResourcePermissionSpecPermissionKind(kind) + switch iamKind { + case iamv0.ResourcePermissionSpecPermissionKindUser: + return zanzana.NewTupleEntry(zanzana.TypeUser, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindServiceAccount: + return zanzana.NewTupleEntry(zanzana.TypeServiceAccount, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindTeam: + return zanzana.NewTupleEntry(zanzana.TypeTeam, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindBasicRole: + basicRole := zanzana.TranslateBasicRole(name) + if basicRole == "" { + return "", fmt.Errorf("%w: %s", errInvalidBasicRole, name) + } + + // e.g role:basic_viewer#assignee + return zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, zanzana.RelationAssignee), nil + } + + // should not happen since we are after create + // validation webhook should have caught invalid kinds + return "", errUnknownKind +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go new file mode 100644 index 00000000000..3336d9d813b --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions_test.go @@ -0,0 +1,115 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateResourcePermissions(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), + common.NewTypedResourceTuple("user:2", common.RelationGet, common.TypeFolder, folderGroup, folderResource, "", "1"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateResourcePermissions(t *testing.T, srv *Server) { + setupMutateResourcePermissions(t, srv) + + t.Run("should create new resource permission", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "foo", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "bar", + Verb: common.RelationGet, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationGet, + Object: "resource:dashboard.grafana.app/dashboards/foo", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:bar", res.Tuples[0].Key.User) + require.Equal(t, common.RelationGet, res.Tuples[0].Key.Relation) + require.Equal(t, "resource:dashboard.grafana.app/dashboards/foo", res.Tuples[0].Key.Object) + }) + + t.Run("should delete resource permission", func(t *testing.T) { + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + User: "user:1", + Object: "resource:dashboard.grafana.app/dashboards/1", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 2) + + _, err = srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "1", + Verb: common.RelationUpdate, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationGet, + Object: "resource:dashboard.grafana.app/dashboards/1", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:1", res.Tuples[0].Key.User) + require.Equal(t, common.RelationGet, res.Tuples[0].Key.Relation) + require.Equal(t, "resource:dashboard.grafana.app/dashboards/1", res.Tuples[0].Key.Object) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_test.go b/pkg/services/authz/zanzana/server/server_mutate_test.go new file mode 100644 index 00000000000..70dc1ea2fb8 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_test.go @@ -0,0 +1,135 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutate(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewFolderParentTuple("11", "1"), + common.NewFolderParentTuple("12", "1"), + common.NewFolderParentTuple("111", "11"), + common.NewFolderParentTuple("112", "11"), + common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), + common.NewTypedResourceTuple("user:2", common.RelationGet, common.TypeFolder, folderGroup, folderResource, "", "1"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutate(t *testing.T, srv *Server) { + setupMutate(t, srv) + + t.Run("should perform multiple mutate operations", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_SetFolderParent{ + SetFolderParent: &v1.SetFolderParentOperation{ + Folder: "new-folder", + Parent: "1", + DeleteExisting: false, + }, + }, + }, + { + Operation: &v1.MutateOperation_CreatePermission{ + CreatePermission: &v1.CreatePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "foo", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "bar", + Verb: common.RelationGet, + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "1", + Verb: common.RelationUpdate, + }, + }, + }, + }, + { + Operation: &v1.MutateOperation_DeletePermission{ + DeletePermission: &v1.DeletePermissionOperation{ + Resource: &v1.Resource{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "1", + }, + Permission: &v1.Permission{ + Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser), + Name: "1", + Verb: common.RelationGet, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Object: "folder:new-folder", + Relation: "parent", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "folder:new-folder", res.Tuples[0].Key.Object) + require.Equal(t, "parent", res.Tuples[0].Key.Relation) + require.Equal(t, "folder:1", res.Tuples[0].Key.User) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationGet, + Object: "resource:dashboard.grafana.app/dashboards/foo", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:bar", res.Tuples[0].Key.User) + require.Equal(t, common.RelationGet, res.Tuples[0].Key.Relation) + require.Equal(t, "resource:dashboard.grafana.app/dashboards/foo", res.Tuples[0].Key.Object) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationGet, + Object: "resource:dashboard.grafana.app/dashboards/1", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_query.go b/pkg/services/authz/zanzana/server/server_query.go new file mode 100644 index 00000000000..596c3805a5a --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_query.go @@ -0,0 +1,91 @@ +package server + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + ctx, span := s.tracer.Start(ctx, "server.Query") + defer span.End() + + defer func(t time.Time) { + s.metrics.requestDurationSeconds.WithLabelValues("server.Query", req.GetNamespace()).Observe(time.Since(t).Seconds()) + }(time.Now()) + + res, err := s.query(ctx, req) + if err != nil { + s.logger.Error("failed to perform query request", "error", err, "namespace", req.GetNamespace()) + return nil, errors.New("failed to perform query request") + } + + return res, nil +} + +func (s *Server) query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { + if err := authorize(ctx, req.GetNamespace(), s.cfg); err != nil { + return nil, err + } + + storeInf, err := s.getStoreInfo(ctx, req.Namespace) + if err != nil { + return nil, fmt.Errorf("failed to get openfga store: %w", err) + } + + if req.Operation == nil { + return nil, errors.New("operation cannot be nil") + } + + switch op := req.Operation.Operation.(type) { + case *authzextv1.QueryOperation_GetFolderParents: + return s.queryFolderParents(ctx, storeInf, op.GetFolderParents) + default: + return nil, errors.New("unsupported query operation type") + } +} + +func (s *Server) queryFolderParents(ctx context.Context, store *storeInfo, req *authzextv1.GetFolderParentsQuery) (*authzextv1.QueryResponse, error) { + ctx, span := s.tracer.Start(ctx, "server.queryFolderParents") + defer span.End() + + if req.GetFolder() == "" { + return nil, errors.New("folder UID cannot be empty") + } + + // Get raw tuples from OpenFGA + tuples, err := s.listFolderParents(ctx, store, req.GetFolder()) + if err != nil { + return nil, fmt.Errorf("failed to list folder parents: %w", err) + } + + // Extract parent UIDs from tuples (business logic now server-side) + parentUIDs := make([]string, 0, len(tuples)) + for _, tuple := range tuples { + // Extract UID from format "folder:UID" or "folder:UID#relation" + userParts := strings.Split(tuple.Key.User, ":") + if len(userParts) != 2 { + return nil, fmt.Errorf("invalid user format: %s, expected format: folder:UID or folder:UID#relation", tuple.Key.User) + } + + // Remove any relation part after # + uidAndRelationParts := strings.Split(userParts[1], "#") + if len(uidAndRelationParts) == 0 { + return nil, fmt.Errorf("invalid user format: %s, expected format: folder:UID or folder:UID#relation", tuple.Key.User) + } + + parentUIDs = append(parentUIDs, uidAndRelationParts[0]) + } + + return &authzextv1.QueryResponse{ + Result: &authzextv1.QueryResponse_FolderParents{ + FolderParents: &authzextv1.GetFolderParentsResult{ + ParentUids: parentUIDs, + }, + }, + }, nil +} diff --git a/pkg/services/authz/zanzana/server/server_query_folder_test.go b/pkg/services/authz/zanzana/server/server_query_folder_test.go new file mode 100644 index 00000000000..5e9284a59df --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_query_folder_test.go @@ -0,0 +1,133 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupFolders() []*openfgav1.TupleKey { + // seed tuples with a folder hierarchy: + // folder 1 (root) + // └── folder 11 + // ├── folder 111 + // └── folder 112 + // └── folder 12 + return []*openfgav1.TupleKey{ + common.NewFolderParentTuple("11", "1"), + common.NewFolderParentTuple("12", "1"), + common.NewFolderParentTuple("111", "11"), + common.NewFolderParentTuple("112", "11"), + } +} + +func setupQueryFolders(t *testing.T, srv *Server) *Server { + t.Helper() + + tuples := []*openfgav1.TupleKey{} + tuples = append(tuples, setupFolders()...) + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testQueryFolders(t *testing.T, srv *Server) { + setupQueryFolders(t, srv) + + t.Run("should query folder parents successfully", func(t *testing.T) { + res, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "11", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotNil(t, res.GetFolderParents()) + require.Len(t, res.GetFolderParents().ParentUids, 1) + require.Equal(t, "1", res.GetFolderParents().ParentUids[0]) + }) + + t.Run("should query nested folder parents successfully", func(t *testing.T) { + res, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "111", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotNil(t, res.GetFolderParents()) + require.Len(t, res.GetFolderParents().ParentUids, 1) + require.Equal(t, "11", res.GetFolderParents().ParentUids[0]) + }) + + t.Run("should return empty list for folder with no parents", func(t *testing.T) { + res, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "1", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotNil(t, res.GetFolderParents()) + require.Len(t, res.GetFolderParents().ParentUids, 0) + }) + + t.Run("should return empty list for non-existent folder", func(t *testing.T) { + res, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "non-existent", + }, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotNil(t, res.GetFolderParents()) + require.Len(t, res.GetFolderParents().ParentUids, 0) + }) + + t.Run("should return error for empty folder UID", func(t *testing.T) { + _, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: &v1.QueryOperation{ + Operation: &v1.QueryOperation_GetFolderParents{ + GetFolderParents: &v1.GetFolderParentsQuery{ + Folder: "", + }, + }, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to perform query request") + }) + + t.Run("should return error for nil operation", func(t *testing.T) { + _, err := srv.Query(newContextWithNamespace(), &v1.QueryRequest{ + Namespace: "default", + Operation: nil, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to perform query request") + }) +} diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 1a9fe263cee..df4f90cf85f 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -44,6 +44,36 @@ const ( statusSubresource = "status" ) +func setup(t *testing.T, srv *Server) *Server { + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), + common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""), + common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""), + common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"), + common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"), + common.NewFolderTuple("user:6", common.RelationGet, "1"), + common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""), + common.NewFolderParentTuple("5", "4"), + common.NewFolderParentTuple("6", "5"), + common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"), + common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"), + common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"), + common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"), + common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource), + common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"), + common.NewFolderResourceTuple("user:13", common.RelationGet, folderGroup, folderResource, statusSubresource, "5"), + common.NewTypedResourceTuple("user:14", common.RelationGet, common.TypeTeam, teamGroup, teamResource, statusSubresource, "1"), + common.NewTypedResourceTuple("user:15", common.RelationGet, common.TypeUser, userGroup, userResource, statusSubresource, "1"), + common.NewTypedResourceTuple("user:16", common.RelationGet, common.TypeServiceAccount, serviceAccountGroup, serviceAccountResource, statusSubresource, "1"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + func TestMain(m *testing.M) { testsuite.Run(m) } @@ -64,27 +94,51 @@ func TestIntegrationServer(t *testing.T) { } } - srv := setup(t, testStore, cfg) + srv := setupOpenFGAServer(t, testStore, cfg) t.Run("test check", func(t *testing.T) { + setup(t, srv) testCheck(t, srv) }) t.Run("test list", func(t *testing.T) { + setup(t, srv) testList(t, srv) }) t.Run("test list streaming", func(t *testing.T) { + setup(t, srv) srv.cfg.UseStreamedListObjects = true testList(t, srv) srv.cfg.UseStreamedListObjects = false }) t.Run("test batch check", func(t *testing.T) { + setup(t, srv) testBatchCheck(t, srv) }) + + t.Run("test mutate", func(t *testing.T) { + testMutate(t, srv) + }) + + t.Run("test mutate folders", func(t *testing.T) { + testMutateFolders(t, srv) + }) + + t.Run("test mutate resource permissions", func(t *testing.T) { + testMutateResourcePermissions(t, srv) + }) + + t.Run("test mutate org roles", func(t *testing.T) { + testMutateOrgRoles(t, srv) + }) + + t.Run("test query folders", func(t *testing.T) { + testQueryFolders(t, srv) + }) } -func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { +func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { t.Helper() store, err := store.NewEmbeddedStore(cfg, testDB, log.NewNopLogger()) @@ -95,38 +149,25 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService(), prometheus.NewRegistry()) require.NoError(t, err) + return srv +} + +func setupOpenFGADatabase(t *testing.T, srv *Server, tuples []*openfgav1.TupleKey) *Server { + t.Helper() + storeInf, err := srv.getStoreInfo(context.Background(), namespace) require.NoError(t, err) + // Clean up any existing store + _, err = srv.openfga.DeleteStore(context.Background(), &openfgav1.DeleteStoreRequest{ + StoreId: storeInf.ID, + }) + require.NoError(t, err) + // seed tuples writes := &openfgav1.WriteRequestWrites{ - TupleKeys: []*openfgav1.TupleKey{ - common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), - common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"), - common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""), - common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""), - common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"), - common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"), - common.NewFolderTuple("user:6", common.RelationGet, "1"), - common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""), - common.NewFolderParentTuple("5", "4"), - common.NewFolderParentTuple("6", "5"), - common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"), - common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"), - common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"), - common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"), - common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource), - common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"), - common.NewFolderResourceTuple("user:13", common.RelationGet, folderGroup, folderResource, statusSubresource, "5"), - common.NewTypedResourceTuple("user:14", common.RelationGet, common.TypeTeam, teamGroup, teamResource, statusSubresource, "1"), - common.NewTypedResourceTuple("user:15", common.RelationGet, common.TypeUser, userGroup, userResource, statusSubresource, "1"), - common.NewTypedResourceTuple("user:16", common.RelationGet, common.TypeServiceAccount, serviceAccountGroup, serviceAccountResource, statusSubresource, "1"), - }, - } - for _, w := range writes.TupleKeys { - t.Log(w.String()) + TupleKeys: tuples, + OnDuplicate: "ignore", } // First, try to delete any existing tuples to avoid conflicts @@ -140,16 +181,18 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { } // Try to delete existing tuples (ignore errors if they don't exist) - _, _ = openfga.Write(context.Background(), &openfgav1.WriteRequest{ + _, err = srv.openfga.Write(context.Background(), &openfgav1.WriteRequest{ StoreId: storeInf.ID, AuthorizationModelId: storeInf.ModelID, Deletes: &openfgav1.WriteRequestDeletes{ TupleKeys: deletes, + OnMissing: "ignore", }, }) + require.NoError(t, err) // Now write the new tuples - _, err = openfga.Write(context.Background(), &openfgav1.WriteRequest{ + _, err = srv.openfga.Write(context.Background(), &openfgav1.WriteRequest{ StoreId: storeInf.ID, AuthorizationModelId: storeInf.ModelID, Writes: writes, diff --git a/pkg/services/authz/zanzana/store.go b/pkg/services/authz/zanzana/store.go deleted file mode 100644 index 67361386744..00000000000 --- a/pkg/services/authz/zanzana/store.go +++ /dev/null @@ -1,18 +0,0 @@ -package zanzana - -import ( - "github.com/openfga/openfga/pkg/storage" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" - - "github.com/grafana/grafana/pkg/services/authz/zanzana/store" -) - -func NewStore(cfg *setting.Cfg, logger log.Logger) (storage.OpenFGADatastore, error) { - return store.NewStore(cfg, logger) -} -func NewEmbeddedStore(cfg *setting.Cfg, db db.DB, logger log.Logger) (storage.OpenFGADatastore, error) { - return store.NewEmbeddedStore(cfg, db, logger) -} diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index a63f581b9ca..6cadfc0c199 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -1,12 +1,6 @@ package zanzana import ( - "fmt" - "strings" - - openfgav1 "github.com/openfga/api/proto/openfga/v1" - - authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) @@ -54,8 +48,8 @@ var ( ) const ( - KindDashboards string = "dashboards" - KindFolders string = "folders" + KindDashboards = common.KindDashboards + KindFolders = common.KindFolders ) var ( @@ -68,128 +62,15 @@ var ( ToOpenFGATuples = common.ToOpenFGATuples ToOpenFGATupleKey = common.ToOpenFGATupleKey ToOpenFGATupleKeyWithoutCondition = common.ToOpenFGATupleKeyWithoutCondition + + NewTupleEntry = common.NewTupleEntry + NewObjectEntry = common.NewObjectEntry + TranslateToResourceTuple = common.TranslateToResourceTuple + IsFolderResourceTuple = common.IsFolderResourceTuple + MergeFolderResourceTuples = common.MergeFolderResourceTuples + + TranslateToCheckRequest = common.TranslateToCheckRequest + TranslateToListRequest = common.TranslateToListRequest + TranslateToGroupResource = common.TranslateToGroupResource + TranslateBasicRole = common.TranslateBasicRole ) - -// NewTupleEntry constructs new openfga entry type:name[#relation]. -// Relation allows to specify group of users (subjects) related to type:name -// (for example, team:devs#member refers to users which are members of team devs) -func NewTupleEntry(objectType, name, relation string) string { - obj := fmt.Sprintf("%s:%s", objectType, name) - if relation != "" { - obj = fmt.Sprintf("%s#%s", obj, relation) - } - return obj -} - -func NewObjectEntry(objectType, group, resource, subresource, name string) string { - if objectType == TypeFolder { - return TypeFolder + ":" + name - } - - obj := fmt.Sprintf("%s:%s/%s", objectType, group, resource) - if subresource != "" { - obj = fmt.Sprintf("%s/%s", obj, subresource) - } - if name != "" { - obj = fmt.Sprintf("%s/%s", obj, name) - } - return obj -} - -func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) { - translation, ok := resourceTranslations[kind] - - if !ok { - return nil, false - } - - m, ok := translation.mapping[action] - if !ok { - return nil, false - } - - if name == "*" { - return common.NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource), true - } - - if translation.typ == TypeResource { - return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource, name), true - } - - if translation.typ == TypeFolder { - if m.group != "" && m.resource != "" { - return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, m.subresource, name), true - } - - return common.NewFolderTuple(subject, m.relation, name), true - } - - return common.NewTypedTuple(translation.typ, subject, m.relation, name), true -} - -func IsFolderResourceTuple(t *openfgav1.TupleKey) bool { - return strings.HasPrefix(t.Object, TypeFolder) && strings.HasPrefix(t.Relation, "resource_") -} - -func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) { - va := a.Condition.Context.Fields["subresources"] - vb := b.Condition.Context.Fields["subresources"] - va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...) -} - -func TranslateToCheckRequest(namespace, action, kind, name string) (*authlib.CheckRequest, bool) { - translation, ok := resourceTranslations[kind] - - if !ok { - return nil, false - } - - m, ok := translation.mapping[action] - if !ok { - return nil, false - } - - verb, ok := common.RelationToVerbMapping[m.relation] - if !ok { - return nil, false - } - - req := &authlib.CheckRequest{ - Namespace: namespace, - Verb: verb, - Group: translation.group, - Resource: translation.resource, - Name: name, - } - - return req, true -} - -func TranslateToListRequest(namespace, action, kind string) (*authlib.ListRequest, bool) { - translation, ok := resourceTranslations[kind] - - if !ok { - return nil, false - } - - // FIXME: support different verbs - req := &authlib.ListRequest{ - Namespace: namespace, - Group: translation.group, - Resource: translation.resource, - } - - return req, true -} - -func TranslateToGroupResource(kind string) string { - translation, ok := resourceTranslations[kind] - if !ok { - return "" - } - return common.FormatGroupResource(translation.group, translation.resource, "") -} - -func TranslateBasicRole(name string) string { - return basicRolesTranslations[name] -} diff --git a/pkg/services/caching/fake_caching_service.go b/pkg/services/caching/fake_caching_service.go index 522be45f02f..d4afc545bc0 100644 --- a/pkg/services/caching/fake_caching_service.go +++ b/pkg/services/caching/fake_caching_service.go @@ -10,19 +10,20 @@ import ( type FakeOSSCachingService struct { calls map[string]int + ReturnStatus CacheStatus ReturnHit bool ReturnResourceResponse CachedResourceDataResponse ReturnQueryResponse CachedQueryDataResponse } -func (f *FakeOSSCachingService) HandleQueryRequest(ctx context.Context, req *backend.QueryDataRequest) (bool, CachedQueryDataResponse) { +func (f *FakeOSSCachingService) HandleQueryRequest(ctx context.Context, req *backend.QueryDataRequest) (bool, CachedQueryDataResponse, CacheStatus) { f.calls["HandleQueryRequest"]++ - return f.ReturnHit, f.ReturnQueryResponse + return f.ReturnHit, f.ReturnQueryResponse, f.ReturnStatus } -func (f *FakeOSSCachingService) HandleResourceRequest(ctx context.Context, req *backend.CallResourceRequest) (bool, CachedResourceDataResponse) { +func (f *FakeOSSCachingService) HandleResourceRequest(ctx context.Context, req *backend.CallResourceRequest) (bool, CachedResourceDataResponse, CacheStatus) { f.calls["HandleResourceRequest"]++ - return f.ReturnHit, f.ReturnResourceResponse + return f.ReturnHit, f.ReturnResourceResponse, f.ReturnStatus } func (f *FakeOSSCachingService) AssertCalls(t *testing.T, fn string, times int) { @@ -35,7 +36,8 @@ func (f *FakeOSSCachingService) Reset() { func NewFakeOSSCachingService() *FakeOSSCachingService { fake := &FakeOSSCachingService{ - calls: map[string]int{}, + calls: map[string]int{}, + ReturnStatus: "unset", } return fake diff --git a/pkg/services/pluginsintegration/clientmiddleware/caching_metrics.go b/pkg/services/caching/metrics.go similarity index 98% rename from pkg/services/pluginsintegration/clientmiddleware/caching_metrics.go rename to pkg/services/caching/metrics.go index 2f452d550c8..ff9c229afbf 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/caching_metrics.go +++ b/pkg/services/caching/metrics.go @@ -1,4 +1,4 @@ -package clientmiddleware +package caching import ( "github.com/grafana/grafana/pkg/infra/metrics" diff --git a/pkg/services/caching/service.go b/pkg/services/caching/service.go index c3da3d22222..c5bc577b122 100644 --- a/pkg/services/caching/service.go +++ b/pkg/services/caching/service.go @@ -7,20 +7,34 @@ import ( "encoding/hex" "encoding/json" "io" + + "strconv" "strings" + "time" + + "github.com/grafana/grafana-aws-sdk/pkg/awsds" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/prometheus/client_golang/prometheus" ) +type CacheStatus string + const ( - XCacheHeader = "X-Cache" - StatusHit = "HIT" - StatusMiss = "MISS" - StatusBypass = "BYPASS" - StatusError = "ERROR" - StatusDisabled = "DISABLED" + XCacheHeader = "X-Cache" + StatusHit CacheStatus = "HIT" + StatusMiss CacheStatus = "MISS" + StatusBypass CacheStatus = "BYPASS" + StatusError CacheStatus = "ERROR" + StatusDisabled CacheStatus = "DISABLED" ) +// needed to mock the function for testing +var ShouldCacheQuery = awsds.ShouldCacheQuery + type CacheQueryResponseFn func(context.Context, *backend.QueryDataResponse) type CacheResourceResponseFn func(context.Context, *backend.CallResourceResponse) @@ -49,28 +63,28 @@ type CachingService interface { // HandleQueryRequest uses a QueryDataRequest to check the cache for any existing results for that query. // If none are found, it should return false and a CachedQueryDataResponse with an UpdateCacheFn which can be used to update the results cache after the fact. // This function may populate any response headers (accessible through the context) with the cache status using the X-Cache header. - HandleQueryRequest(context.Context, *backend.QueryDataRequest) (bool, CachedQueryDataResponse) + HandleQueryRequest(ctx context.Context, req *backend.QueryDataRequest) (bool, CachedQueryDataResponse, CacheStatus) // HandleResourceRequest uses a CallResourceRequest to check the cache for any existing results for that request. If none are found, it should return false. // This function may populate any response headers (accessible through the context) with the cache status using the X-Cache header. - HandleResourceRequest(context.Context, *backend.CallResourceRequest) (bool, CachedResourceDataResponse) + HandleResourceRequest(ctx context.Context, req *backend.CallResourceRequest) (bool, CachedResourceDataResponse, CacheStatus) } // Implementation of interface - does nothing type OSSCachingService struct { } -func (s *OSSCachingService) HandleQueryRequest(ctx context.Context, req *backend.QueryDataRequest) (bool, CachedQueryDataResponse) { - return false, CachedQueryDataResponse{} +func (s *OSSCachingService) HandleQueryRequest(ctx context.Context, req *backend.QueryDataRequest) (bool, CachedQueryDataResponse, CacheStatus) { + return false, CachedQueryDataResponse{}, "" } -func (s *OSSCachingService) HandleResourceRequest(ctx context.Context, req *backend.CallResourceRequest) (bool, CachedResourceDataResponse) { - return false, CachedResourceDataResponse{} +func (s *OSSCachingService) HandleResourceRequest(ctx context.Context, req *backend.CallResourceRequest) (bool, CachedResourceDataResponse, CacheStatus) { + return false, CachedResourceDataResponse{}, "" } var _ CachingService = &OSSCachingService{} // GetKey creates a prefixed cache key and uses the internal `encoder` to encode the query into a string -func GetKey(prefix string, query interface{}) (string, error) { +func GetKey(namespace, prefix string, query interface{}) (string, error) { keybuf := bytes.NewBuffer(nil) encoder := &JSONEncoder{} @@ -84,6 +98,12 @@ func GetKey(prefix string, query interface{}) (string, error) { return "", err } + // The namespace is empty only when this function is used by the legacy caching module. + // This case can be removed when the legacy caching module is not being used anymore. + if namespace != "" { + return strings.Join([]string{namespace, prefix, key}, ":"), nil + } + return strings.Join([]string{prefix, key}, ":"), nil } @@ -133,3 +153,134 @@ func (e *JSONEncoder) Encode(w io.Writer, v interface{}) error { func (e *JSONEncoder) Decode(r io.Reader, v interface{}) error { return json.NewDecoder(r).Decode(v) } + +// A service that provides methods to cache requests. +// It can be used to cache requests using `caching.CachingService` without reimplementing +// the caching logic at every call site. +type CachingServiceClient struct { + cachingService CachingService + features featuremgmt.FeatureToggles +} + +func ProvideCachingServiceClient(cachingService CachingService, features featuremgmt.FeatureToggles) *CachingServiceClient { + log := log.New("caching_service_client") + if err := prometheus.Register(QueryCachingRequestHistogram); err != nil { + log.Error("Error registering prometheus collector 'QueryRequestHistogram'", "error", err) + } + if err := prometheus.Register(ResourceCachingRequestHistogram); err != nil { + log.Error("Error registering prometheus collector 'ResourceRequestHistogram'", "error", err) + } + return &CachingServiceClient{cachingService: cachingService, features: features} +} + +// WithQueryDataCaching calls `f` and caches the returned value if `req` has not been cached already. +// Returns the cached value otherwise. +func (c *CachingServiceClient) WithQueryDataCaching(ctx context.Context, req *backend.QueryDataRequest, f func() (*backend.QueryDataResponse, error)) (*backend.QueryDataResponse, error) { + if c == nil || req == nil { + return f() + } + + reqCtx := contexthandler.FromContext(ctx) + + // time how long this request takes + start := time.Now() + + // First look in the query cache if enabled + hit, cr, status := c.cachingService.HandleQueryRequest(ctx, req) + + // record request duration if caching was used + if reqCtx != nil { + reqCtx.Resp.Header().Set(XCacheHeader, string(status)) + defer func() { + QueryCachingRequestHistogram.With(prometheus.Labels{ + "datasource_type": getDatasourceType(req.PluginContext), + "cache": string(status), + "query_type": getQueryType(reqCtx), + }).Observe(time.Since(start).Seconds()) + }() + } + + // Cache hit; return the response + if hit { + return cr.Response, nil + } + + // Cache miss; do the actual queries + resp, err := f() + // Update the query cache with the result for this metrics request + if err == nil && cr.UpdateCacheFn != nil { + // If AWS async caching is not enabled, use the old code path + if c.features == nil || !c.features.IsEnabled(ctx, featuremgmt.FlagAwsAsyncQueryCaching) { + cr.UpdateCacheFn(ctx, resp) + } else if reqCtx != nil { + // time how long shouldCacheQuery takes + startShouldCacheQuery := time.Now() + shouldCache := ShouldCacheQuery(resp) + ShouldCacheQueryHistogram.With(prometheus.Labels{ + "datasource_type": req.PluginContext.DataSourceInstanceSettings.Type, + "cache": string(status), + "shouldCache": strconv.FormatBool(shouldCache), + "query_type": getQueryType(reqCtx), + }).Observe(time.Since(startShouldCacheQuery).Seconds()) + + // If AWS async caching is enabled and resp is for a running async query, don't cache it + if shouldCache { + cr.UpdateCacheFn(ctx, resp) + } + } + } + + return resp, err +} + +// WithCallResourceCaching calls `f` and caches the returned value if `req` has not been cached already. +// Returns the cached value otherwise. +func (c *CachingServiceClient) WithCallResourceCaching(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender, f func(backend.CallResourceResponseSender) error) error { + if c == nil || req == nil { + return f(sender) + } + + reqCtx := contexthandler.FromContext(ctx) + + // time how long this request takes + start := time.Now() + + // First look in the resource cache if enabled + hit, cr, status := c.cachingService.HandleResourceRequest(ctx, req) + + if reqCtx != nil { + reqCtx.Resp.Header().Set(XCacheHeader, string(status)) + } + // record request duration if caching was used + defer func() { + ResourceCachingRequestHistogram.With(prometheus.Labels{ + "plugin_id": req.PluginContext.PluginID, + "cache": string(status), + }).Observe(time.Since(start).Seconds()) + }() + + // Cache hit; send the response and return + if hit { + return sender.Send(cr.Response) + } + + // Cache miss; do the actual request + // If there is no update cache func, just pass in the original sender + if cr.UpdateCacheFn == nil { + return f(sender) + } + // Otherwise, intercept the responses in a wrapped sender so we can cache them first + cacheSender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { + cr.UpdateCacheFn(ctx, res) + return sender.Send(res) + }) + + return f(cacheSender) +} + +func getDatasourceType(pluginCtx backend.PluginContext) string { + if pluginCtx.DataSourceInstanceSettings == nil { + return "unknown" + } + return pluginCtx.DataSourceInstanceSettings.Name +} diff --git a/pkg/services/caching/service_test.go b/pkg/services/caching/service_test.go new file mode 100644 index 00000000000..90864171469 --- /dev/null +++ b/pkg/services/caching/service_test.go @@ -0,0 +1,130 @@ +package caching + +import ( + "context" + "errors" + "net/http/httptest" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/web" + "github.com/stretchr/testify/require" +) + +func TestWithQueryDataCaching(t *testing.T) { + t.Run("caching is a no-op when service is nil", func(t *testing.T) { + var s *CachingServiceClient + req := backend.QueryDataRequest{} + fakeResponse := &backend.QueryDataResponse{} + response, err := s.WithQueryDataCaching(t.Context(), &req, func() (*backend.QueryDataResponse, error) { + return fakeResponse, nil + }) + require.NoError(t, err) + require.Equal(t, fakeResponse, response) + }) + + t.Run("cache status is included in the response if a request context is available", func(t *testing.T) { + fakeCachingService := NewFakeOSSCachingService() + fakeCachingService.ReturnStatus = StatusMiss + client := ProvideCachingServiceClient(fakeCachingService, nil) + + req := backend.QueryDataRequest{} + + reqCtx := &contextmodel.ReqContext{ + Context: &web.Context{ + Resp: web.NewResponseWriter("", httptest.NewRecorder()), + }, + } + ctx := context.WithValue(t.Context(), ctxkey.Key{}, reqCtx) + fakeResponse := &backend.QueryDataResponse{} + response, err := client.WithQueryDataCaching(ctx, &req, func() (*backend.QueryDataResponse, error) { + return fakeResponse, nil + }) + require.NoError(t, err) + require.Equal(t, fakeResponse, response) + require.EqualValues(t, StatusMiss, reqCtx.Resp.Header().Get(XCacheHeader)) + }) + + t.Run("caching can be used without a request context", func(t *testing.T) { + fakeCachingService := NewFakeOSSCachingService() + fakeCachingService.ReturnStatus = StatusMiss + client := ProvideCachingServiceClient(fakeCachingService, nil) + + req := backend.QueryDataRequest{} + + fakeResponse := &backend.QueryDataResponse{} + // Using the default test context, no request context. + response, err := client.WithQueryDataCaching(t.Context(), &req, func() (*backend.QueryDataResponse, error) { + return fakeResponse, nil + }) + require.NoError(t, err) + require.Equal(t, fakeResponse, response) + }) +} + +func TestWithCallResourceCaching(t *testing.T) { + t.Run("caching is a no-op when service is nil", func(t *testing.T) { + var s *CachingServiceClient + req := backend.CallResourceRequest{} + fakeErr := errors.New("oops") + err := s.WithCallResourceCaching(t.Context(), &req, nil, func(backend.CallResourceResponseSender) error { + return fakeErr + }) + require.ErrorIs(t, err, fakeErr) + }) + + t.Run("cache status is included in the response if a request context is available", func(t *testing.T) { + fakeCachingService := NewFakeOSSCachingService() + fakeCachingService.ReturnStatus = StatusMiss + client := ProvideCachingServiceClient(fakeCachingService, nil) + + req := backend.CallResourceRequest{} + + reqCtx := &contextmodel.ReqContext{ + Context: &web.Context{ + Resp: web.NewResponseWriter("", httptest.NewRecorder()), + }, + } + ctx := context.WithValue(t.Context(), ctxkey.Key{}, reqCtx) + sender := func(*backend.CallResourceResponse) error { + return nil + } + var fakeErr = errors.New("oops") + err := client.WithCallResourceCaching(ctx, &req, backend.CallResourceResponseSenderFunc(sender), func(backend.CallResourceResponseSender) error { + return fakeErr + }) + require.ErrorIs(t, err, fakeErr) + require.EqualValues(t, StatusMiss, reqCtx.Resp.Header().Get(XCacheHeader)) + }) + + t.Run("caching can be used without a request context", func(t *testing.T) { + fakeCachingService := NewFakeOSSCachingService() + fakeCachingService.ReturnStatus = StatusMiss + client := ProvideCachingServiceClient(fakeCachingService, nil) + + req := backend.CallResourceRequest{} + + sender := func(*backend.CallResourceResponse) error { + return nil + } + var fakeErr = errors.New("oops") + // Using the default test context, no request context. + err := client.WithCallResourceCaching(t.Context(), &req, backend.CallResourceResponseSenderFunc(sender), func(_ backend.CallResourceResponseSender) error { + return fakeErr + }) + require.ErrorIs(t, err, fakeErr) + }) +} + +func TestGetDatasourceType(t *testing.T) { + t.Parallel() + + require.Equal(t, "unknown", getDatasourceType(backend.PluginContext{})) + require.Equal(t, "name", getDatasourceType(backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + Name: "name", + }, + })) +} diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index cc73faa7b12..fde4ab729d5 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -14,10 +14,9 @@ import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" - "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1" + "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -326,11 +325,7 @@ func (srv *CleanUpService) deleteStaleKubernetesShortURLs(ctx context.Context) { } // Set up the GroupVersionResource for shortURLs - gvr := schema.GroupVersionResource{ - Group: v1alpha1.ShortURLKind().Group(), - Version: v1alpha1.ShortURLKind().Version(), - Resource: v1alpha1.ShortURLKind().Plural(), - } + gvr := v1beta1.ShortURLKind().GroupVersionResource() // Calculate the expiration time expirationTime := time.Now().Add(-time.Duration(srv.Cfg.ShortLinkExpiration*24) * time.Hour) @@ -355,7 +350,7 @@ func (srv *CleanUpService) deleteStaleKubernetesShortURLs(ctx context.Context) { // Check each shortURL for expiration for _, item := range shortURLs.Items { // Convert unstructured object to ShortURL struct - var shortURL v1alpha1.ShortURL + var shortURL v1beta1.ShortURL err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, &shortURL) if err != nil { logger.Error("Failed to convert unstructured object to ShortURL", "name", item.GetName(), "namespace", item.GetNamespace(), "error", err.Error()) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 449a0ee0817..2d393236801 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -40,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ngalertstore "github.com/grafana/grafana/pkg/services/ngalert/store" ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -790,7 +791,15 @@ func TestGetPlugins(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - user := &user.SignedInUser{OrgID: 1} + user := &user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{ + 1: { + pluginaccesscontrol.ActionInstall: {pluginaccesscontrol.ScopeProvider.GetResourceAllScope()}, + pluginaccesscontrol.ActionWrite: {pluginaccesscontrol.ScopeProvider.GetResourceAllScope()}, + }, + }, + } s.pluginStore = pluginstore.NewFakePluginStore([]pluginstore.Plugin{ { @@ -1001,7 +1010,7 @@ func setUpServiceTest(t *testing.T, cfgOverrides ...configOverrides) cloudmigrat mockFolder, &pluginstore.FakePluginStore{}, &pluginsettings.FakePluginSettings{}, - actest.FakeAccessControl{ExpectedEvaluate: true}, + accessControl, fakeAccessControlService, kvstore.ProvideService(sqlStore), &libraryelementsfake.LibraryElementService{}, diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go index a279c09d9db..ae9a1ef52ed 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go @@ -9,9 +9,12 @@ import ( "github.com/prometheus/common/model" "github.com/grafana/grafana/pkg/components/simplejson" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" ngalertapi "github.com/grafana/grafana/pkg/services/ngalert/api/compat" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -79,6 +82,13 @@ type contactPoint struct { } func (s *Service) getContactPoints(ctx context.Context, signedInUser *user.SignedInUser) ([]contactPoint, error) { + userIsOrgAdmin := signedInUser.HasRole(org.RoleAdmin) + hasAccess, _ := s.accessControl.Evaluate(ctx, signedInUser, ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets, models.ScopeReceiversAll)) + if !userIsOrgAdmin && !hasAccess { + msg := "user '%s' is not allowed to read contact point secrets, missing 'alert.notifications.receivers.secrets:read' permission, which can be granted through the 'Admin' or 'Alerting > Full admin access' roles" + return nil, fmt.Errorf(msg, signedInUser.UserUID) + } + query := provisioning.ContactPointQuery{ OrgID: signedInUser.GetOrgID(), Decrypt: true, // needed to recreate the settings in the target instance. diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index ec50629c24b..611b10c317b 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "net/http" "testing" "time" @@ -13,7 +12,6 @@ import ( "github.com/prometheus/alertmanager/pkg/labels" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" @@ -127,10 +125,7 @@ func TestGetContactPoints(t *testing.T) { contactPoints, err := s.getContactPoints(ctx, user) require.Nil(t, contactPoints) - - gfErr := errutil.Error{} - require.ErrorAs(t, err, &gfErr) - require.Equal(t, http.StatusForbidden, gfErr.Reason.Status().HTTPStatus()) + require.Contains(t, err.Error(), "alert.notifications.receivers.secrets:read") }) } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index 2d84b257a5a..6abb63ce54e 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -822,7 +822,7 @@ func TestEncodeDecode(t *testing.T) { Private: grafanaPrivateKey[:], }, crypto.NewNacl(), - "", + t.TempDir(), ) require.NoError(t, err) diff --git a/pkg/services/dashboardimport/api/api.go b/pkg/services/dashboardimport/api/api.go index 27697602383..b0dabcaf881 100644 --- a/pkg/services/dashboardimport/api/api.go +++ b/pkg/services/dashboardimport/api/api.go @@ -47,7 +47,7 @@ func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteR routing.Wrap(api.ImportDashboard), ) //nolint:staticcheck // not yet migrated to OpenFeature - if api.features.IsEnabledGlobally(featuremgmt.FlagDashboardLibrary) { + if api.features.IsEnabledGlobally(featuremgmt.FlagDashboardLibrary) || api.features.IsEnabledGlobally(featuremgmt.FlagSuggestedDashboards) { route.Post( "/interpolate", authorize(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate)), @@ -59,7 +59,7 @@ func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteR // swagger:route POST /dashboards/interpolate dashboards interpolateDashboard // -// Interpolate dashboard. This is an experimental endpoint under dashboardLibrary FF and is subject to change. +// Interpolate dashboard. This is an experimental endpoint under dashboardLibrary or suggestedDashboards feature flags and is subject to change. // // Responses: // 200: interpolateDashboardResponse @@ -73,8 +73,8 @@ func (api *ImportDashboardAPI) InterpolateDashboard(c *contextmodel.ReqContext) return response.Error(http.StatusBadRequest, "bad request data", err) } - if req.PluginId == "" { - return response.Error(http.StatusUnprocessableEntity, "pluginId must be set", nil) + if req.PluginId == "" && req.Dashboard == nil { + return response.Error(http.StatusUnprocessableEntity, "pluginId or dashboard must be set", nil) } resp, err := api.dashboardImportService.InterpolateDashboard(c.Req.Context(), &req) diff --git a/pkg/services/dashboardimport/api/api_test.go b/pkg/services/dashboardimport/api/api_test.go index 17e0e5d1685..461e370352e 100644 --- a/pkg/services/dashboardimport/api/api_test.go +++ b/pkg/services/dashboardimport/api/api_test.go @@ -189,7 +189,7 @@ func TestInterpolateDashboardFeatureFlag(t *testing.T) { require.Equal(t, http.StatusNotFound, resp.StatusCode) }) - t.Run("Feature flag enabled - interpolate endpoint should work", func(t *testing.T) { + t.Run("dashboardLibrary feature flag enabled - interpolate endpoint should work", func(t *testing.T) { interpolateDashboardServiceCalled := false service := &serviceMock{ interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) { @@ -223,6 +223,232 @@ func TestInterpolateDashboardFeatureFlag(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) require.True(t, interpolateDashboardServiceCalled) }) + + t.Run("suggestedDashboards feature flag enabled - interpolate endpoint should work", func(t *testing.T) { + interpolateDashboardServiceCalled := false + service := &serviceMock{ + interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) { + interpolateDashboardServiceCalled = true + return simplejson.New(), nil + }, + } + // Create features with suggestedDashboards enabled + features := featuremgmt.WithFeatures(featuremgmt.FlagSuggestedDashboards) + importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features) + + routeRegister := routing.NewRouteRegister() + importDashboardAPI.RegisterAPIEndpoints(routeRegister) + s := webtest.NewServer(t, routeRegister) + + cmd := &dashboardimport.ImportDashboardRequest{ + PluginId: "test-plugin", + } + jsonBytes, err := json.Marshal(cmd) + require.NoError(t, err) + req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes)) + webtest.RequestWithSignedInUser(req, &user.SignedInUser{ + UserID: 1, + Permissions: map[int64]map[string][]string{ + 1: {dashboards.ActionDashboardsCreate: {}}, + }, + }) + resp, err := s.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.True(t, interpolateDashboardServiceCalled) + }) +} + +func TestInterpolateDashboardAPI(t *testing.T) { + features := featuremgmt.WithFeatures(featuremgmt.FlagDashboardLibrary) + + t.Run("Backward compatibility - plugin-based flow still works", func(t *testing.T) { + var capturedReq *dashboardimport.ImportDashboardRequest + interpolateDashboardServiceCalled := false + service := &serviceMock{ + interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) { + interpolateDashboardServiceCalled = true + capturedReq = req + result := simplejson.New() + result.Set("title", "Test Dashboard") + return result, nil + }, + } + importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features) + + routeRegister := routing.NewRouteRegister() + importDashboardAPI.RegisterAPIEndpoints(routeRegister) + s := webtest.NewServer(t, routeRegister) + + cmd := &dashboardimport.ImportDashboardRequest{ + PluginId: "test-plugin", + Path: "dashboards/test.json", + } + jsonBytes, err := json.Marshal(cmd) + require.NoError(t, err) + req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes)) + webtest.RequestWithSignedInUser(req, &user.SignedInUser{ + UserID: 1, + Permissions: map[int64]map[string][]string{ + 1: {dashboards.ActionDashboardsCreate: {}}, + }, + }) + resp, err := s.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.True(t, interpolateDashboardServiceCalled) + require.NotNil(t, capturedReq) + require.Equal(t, "test-plugin", capturedReq.PluginId) + require.Equal(t, "dashboards/test.json", capturedReq.Path) + }) + + t.Run("New flow with dashboard JSON - should call service with dashboard", func(t *testing.T) { + var capturedReq *dashboardimport.ImportDashboardRequest + interpolateDashboardServiceCalled := false + service := &serviceMock{ + interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) { + interpolateDashboardServiceCalled = true + capturedReq = req + result := simplejson.New() + result.Set("title", "Community Dashboard") + result.Set("panels", []interface{}{}) + return result, nil + }, + } + importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features) + + routeRegister := routing.NewRouteRegister() + importDashboardAPI.RegisterAPIEndpoints(routeRegister) + s := webtest.NewServer(t, routeRegister) + + // Create a test dashboard with datasource that needs interpolation + testDashboard := simplejson.New() + testDashboard.Set("title", "Test Community Dashboard") + testDashboard.Set("panels", []interface{}{ + map[string]interface{}{ + "datasource": "${DS_PROMETHEUS}", + }, + }) + + cmd := &dashboardimport.ImportDashboardRequest{ + Dashboard: testDashboard, + Inputs: []dashboardimport.ImportDashboardInput{ + {Name: "DS_PROMETHEUS", Type: "datasource", PluginId: "prometheus", Value: "my-prometheus"}, + }, + } + jsonBytes, err := json.Marshal(cmd) + require.NoError(t, err) + req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes)) + webtest.RequestWithSignedInUser(req, &user.SignedInUser{ + UserID: 1, + Permissions: map[int64]map[string][]string{ + 1: {dashboards.ActionDashboardsCreate: {}}, + }, + }) + resp, err := s.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.True(t, interpolateDashboardServiceCalled) + require.NotNil(t, capturedReq) + require.NotNil(t, capturedReq.Dashboard) + require.Len(t, capturedReq.Inputs, 1) + require.Equal(t, "DS_PROMETHEUS", capturedReq.Inputs[0].Name) + }) + + t.Run("Validation - both pluginId and dashboard missing should return error", func(t *testing.T) { + service := &serviceMock{} + importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features) + + routeRegister := routing.NewRouteRegister() + importDashboardAPI.RegisterAPIEndpoints(routeRegister) + s := webtest.NewServer(t, routeRegister) + + cmd := &dashboardimport.ImportDashboardRequest{ + PluginId: "", + Dashboard: nil, + } + jsonBytes, err := json.Marshal(cmd) + require.NoError(t, err) + req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes)) + webtest.RequestWithSignedInUser(req, &user.SignedInUser{ + UserID: 1, + Permissions: map[int64]map[string][]string{ + 1: {dashboards.ActionDashboardsCreate: {}}, + }, + }) + resp, err := s.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) + }) + + t.Run("Response should not include internal fields", func(t *testing.T) { + service := &serviceMock{ + interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) { + result := simplejson.New() + result.Set("title", "Test Dashboard") + result.Set("__elements", map[string]interface{}{"test": "value"}) + result.Set("__inputs", []interface{}{}) + result.Set("__requires", []interface{}{}) + result.Set("panels", []interface{}{}) + return result, nil + }, + } + importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features) + + routeRegister := routing.NewRouteRegister() + importDashboardAPI.RegisterAPIEndpoints(routeRegister) + s := webtest.NewServer(t, routeRegister) + + cmd := &dashboardimport.ImportDashboardRequest{ + PluginId: "test-plugin", + } + jsonBytes, err := json.Marshal(cmd) + require.NoError(t, err) + req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes)) + webtest.RequestWithSignedInUser(req, &user.SignedInUser{ + UserID: 1, + Permissions: map[int64]map[string][]string{ + 1: {dashboards.ActionDashboardsCreate: {}}, + }, + }) + resp, err := s.SendJSON(req) + require.NoError(t, err) + defer func() { require.NoError(t, resp.Body.Close()) }() + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Parse response body and verify internal fields are removed + var responseBody map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&responseBody) + require.NoError(t, err) + require.Equal(t, "Test Dashboard", responseBody["title"]) + require.NotContains(t, responseBody, "__elements") + require.NotContains(t, responseBody, "__inputs") + require.NotContains(t, responseBody, "__requires") + }) + + t.Run("Not signed in should return 401", func(t *testing.T) { + service := &serviceMock{} + importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features) + + routeRegister := routing.NewRouteRegister() + importDashboardAPI.RegisterAPIEndpoints(routeRegister) + s := webtest.NewServer(t, routeRegister) + + cmd := &dashboardimport.ImportDashboardRequest{ + Dashboard: simplejson.New(), + } + jsonBytes, err := json.Marshal(cmd) + require.NoError(t, err) + req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes)) + resp, err := s.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) } type serviceMock struct { diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index 8e78bdcac3d..ca006d0b98b 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -60,8 +61,10 @@ func (s *ImportDashboardService) InterpolateDashboard(ctx context.Context, req * } else { draftDashboard = resp.Dashboard } - } else { + } else if req.Dashboard != nil { draftDashboard = dashboards.NewDashboardFromJson(req.Dashboard) + } else { + return nil, fmt.Errorf("either PluginId or Dashboard must be provided") } evaluator := utils.NewDashTemplateEvaluator(draftDashboard.Data, req.Inputs) diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index 16ee483fc1b..76c02f424bc 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -160,6 +160,159 @@ func TestImportDashboardService(t *testing.T) { }) } +func TestInterpolateDashboardService(t *testing.T) { + t.Run("InterpolateDashboard with plugin ID should load from plugin", func(t *testing.T) { + pluginDashboardService := &pluginDashboardServiceMock{ + loadPluginDashboardFunc: loadTestDashboard, + } + + s := &ImportDashboardService{ + pluginDashboardService: pluginDashboardService, + features: featuremgmt.WithFeatures(), + } + + req := &dashboardimport.ImportDashboardRequest{ + PluginId: "prometheus", + Path: "dashboard.json", + Inputs: []dashboardimport.ImportDashboardInput{ + {Name: "*", Type: "datasource", Value: "prom"}, + }, + } + + result, err := s.InterpolateDashboard(context.Background(), req) + require.NoError(t, err) + require.NotNil(t, result) + + // Verify datasource was interpolated + panel := result.Get("panels").GetIndex(0) + require.Equal(t, "prom", panel.Get("datasource").MustString()) + }) + + t.Run("InterpolateDashboard with dashboard JSON should apply interpolation", func(t *testing.T) { + s := &ImportDashboardService{ + features: featuremgmt.WithFeatures(), + } + + // Create test dashboard with template variables + testDashboard := simplejson.New() + testDashboard.Set("title", "Test Community Dashboard") + testDashboard.Set("uid", "test-uid") + + // Add __inputs section (required by template evaluator) + inputs := []interface{}{ + map[string]interface{}{ + "name": "DS_PROMETHEUS", + "type": "datasource", + "pluginId": "prometheus", + }, + map[string]interface{}{ + "name": "DS_LOKI", + "type": "datasource", + "pluginId": "loki", + }, + } + testDashboard.Set("__inputs", inputs) + + panels := []interface{}{ + map[string]interface{}{ + "id": 1, + "datasource": map[string]interface{}{ + "uid": "${DS_PROMETHEUS}", + }, + }, + map[string]interface{}{ + "id": 2, + "datasource": map[string]interface{}{ + "uid": "${DS_LOKI}", + }, + }, + } + testDashboard.Set("panels", panels) + + req := &dashboardimport.ImportDashboardRequest{ + Dashboard: testDashboard, + Inputs: []dashboardimport.ImportDashboardInput{ + {Name: "DS_PROMETHEUS", Type: "datasource", PluginId: "prometheus", Value: "my-prometheus"}, + {Name: "DS_LOKI", Type: "datasource", PluginId: "loki", Value: "my-loki"}, + }, + } + + result, err := s.InterpolateDashboard(context.Background(), req) + require.NoError(t, err) + require.NotNil(t, result) + + // Verify datasources were interpolated correctly + panel1 := result.Get("panels").GetIndex(0) + require.Equal(t, "my-prometheus", panel1.Get("datasource").Get("uid").MustString()) + + panel2 := result.Get("panels").GetIndex(1) + require.Equal(t, "my-loki", panel2.Get("datasource").Get("uid").MustString()) + }) + + t.Run("InterpolateDashboard with dashboard JSON and wildcard datasource", func(t *testing.T) { + s := &ImportDashboardService{ + features: featuremgmt.WithFeatures(), + } + + // Create test dashboard with simple datasource reference + testDashboard := simplejson.New() + testDashboard.Set("title", "Test Dashboard") + + // Add __inputs section for wildcard matching + inputs := []interface{}{ + map[string]interface{}{ + "name": "DS_TEST", + "type": "datasource", + "pluginId": "testdata", + }, + } + testDashboard.Set("__inputs", inputs) + + panels := []interface{}{ + map[string]interface{}{ + "id": 1, + "datasource": "${DS_TEST}", + }, + } + testDashboard.Set("panels", panels) + + req := &dashboardimport.ImportDashboardRequest{ + Dashboard: testDashboard, + Inputs: []dashboardimport.ImportDashboardInput{ + {Name: "*", Type: "datasource", Value: "default-datasource"}, + }, + } + + result, err := s.InterpolateDashboard(context.Background(), req) + require.NoError(t, err) + require.NotNil(t, result) + + // With wildcard, it should replace any datasource template + panel := result.Get("panels").GetIndex(0) + datasource := panel.Get("datasource").MustString() + // The wildcard matcher should have replaced the template + require.NotEqual(t, "${DS_TEST}", datasource) + }) + + t.Run("InterpolateDashboard without plugin ID or dashboard should fail", func(t *testing.T) { + s := &ImportDashboardService{ + features: featuremgmt.WithFeatures(), + } + + req := &dashboardimport.ImportDashboardRequest{ + PluginId: "", + Dashboard: nil, + Inputs: []dashboardimport.ImportDashboardInput{}, + } + + // This should fail with validation error + result, err := s.InterpolateDashboard(context.Background(), req) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "either PluginId or Dashboard must be provided") + }) +} + func loadTestDashboard(ctx context.Context, req *plugindashboards.LoadPluginDashboardRequest) (*plugindashboards.LoadPluginDashboardResponse, error) { // It's safe to ignore gosec warning G304 since this is a test and arguments comes from test configuration. // nolint:gosec diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 4e077e08d9d..0334cfc8990 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -87,6 +87,7 @@ type Store interface { GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioningSearchResults, error) GetProvisionedDashboardsByName(ctx context.Context, name string, orgID int64) ([]*DashboardProvisioningSearchResults, error) GetOrphanedProvisionedDashboards(ctx context.Context, notIn []string, orgID int64) ([]*Dashboard, error) + GetDuplicateProvisionedDashboards(ctx context.Context) ([]*DashboardProvisioningSearchResults, error) SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) UnprovisionDashboard(ctx context.Context, id int64) error diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 490bc51f475..ea37867d8c0 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -209,7 +209,7 @@ func (d *dashboardStore) GetProvisionedDataByDashboardID(ctx context.Context, da return sess.Table(`dashboard`). Join(`INNER`, `dashboard_provisioning`, `dashboard.id = dashboard_provisioning.dashboard_id`). Where(`dashboard_provisioning.dashboard_id = ?`, dashboardID). - Select("dashboard.*, dashboard_provisioning.name, dashboard_provisioning.external_id, dashboard_provisioning.updated as provisioning_updated, dashboard_provisioning.check_sum"). + Select("dashboard.id, dashboard.uid, dashboard.title, dashboard.folder_uid, dashboard.org_id, dashboard_provisioning.name, dashboard_provisioning.external_id, dashboard_provisioning.updated as provisioning_updated, dashboard_provisioning.check_sum"). Find(&data) }) if err != nil { @@ -241,7 +241,7 @@ func (d *dashboardStore) GetProvisionedDataByDashboardUID(ctx context.Context, o return sess.Table(`dashboard`). Join(`INNER`, `dashboard_provisioning`, `dashboard.id = dashboard_provisioning.dashboard_id`). Where(`dashboard_provisioning.dashboard_id = ?`, dashboard.ID). - Select("dashboard.*, dashboard_provisioning.name, dashboard_provisioning.external_id, dashboard_provisioning.updated as provisioning_updated, dashboard_provisioning.check_sum"). + Select("dashboard.id, dashboard.uid, dashboard.title, dashboard.folder_uid, dashboard.org_id, dashboard_provisioning.name, dashboard_provisioning.external_id, dashboard_provisioning.updated as provisioning_updated, dashboard_provisioning.check_sum"). Find(&provisionedDashboard) }) if err != nil { @@ -275,7 +275,7 @@ func (d *dashboardStore) GetProvisionedDashboardsByName(ctx context.Context, nam return sess.Table(`dashboard`). Join(`INNER`, `dashboard_provisioning`, `dashboard.id = dashboard_provisioning.dashboard_id`). Where(`dashboard_provisioning.name = ? AND dashboard.org_id = ?`, name, orgID). - Select("dashboard.*, dashboard_provisioning.name, dashboard_provisioning.external_id, dashboard_provisioning.updated as provisioning_updated, dashboard_provisioning.check_sum"). + Select("dashboard.id, dashboard.uid, dashboard.title, dashboard.folder_uid, dashboard.org_id, dashboard_provisioning.name, dashboard_provisioning.external_id, dashboard_provisioning.updated as provisioning_updated, dashboard_provisioning.check_sum"). Find(&dashes) }) if err != nil { @@ -301,6 +301,56 @@ func (d *dashboardStore) GetOrphanedProvisionedDashboards(ctx context.Context, n return dashes, nil } +func (d *dashboardStore) GetDuplicateProvisionedDashboards(ctx context.Context) ([]*dashboards.DashboardProvisioningSearchResults, error) { + ctx, span := tracer.Start(ctx, "dashboards.database.GetDuplicateProvisionedDashboards") + defer span.End() + + dashes := []*dashboards.DashboardProvisioningSearchResults{} + err := d.store.WithDbSession(ctx, func(sess *db.Session) error { + type duplicateGroup struct { + Name string `xorm:"name"` + ExternalID string `xorm:"external_id"` + CheckSum string `xorm:"check_sum"` + } + + duplicateGroups := []duplicateGroup{} + err := sess.SQL(` + SELECT dp.name, dp.external_id + FROM dashboard_provisioning dp + INNER JOIN dashboard d ON d.id = dp.dashboard_id + GROUP BY dp.name, dp.external_id, dp.check_sum + HAVING COUNT(*) > 1 + `).Find(&duplicateGroups) + + if err != nil { + return err + } + + if len(duplicateGroups) == 0 { + return nil + } + + for _, group := range duplicateGroups { + var groupDashes []*dashboards.DashboardProvisioningSearchResults + err := sess.Table(`dashboard`). + Join(`INNER`, `dashboard_provisioning`, `dashboard.id = dashboard_provisioning.dashboard_id`). + Where(`dashboard_provisioning.name = ? AND dashboard_provisioning.external_id = ?`, group.Name, group.ExternalID). + Find(&groupDashes) + if err != nil { + return err + } + dashes = append(dashes, groupDashes...) + } + + return nil + }) + + if err != nil { + return nil, err + } + return dashes, nil +} + func (d *dashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand, provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) { ctx, span := tracer.Start(ctx, "dashboards.database.SaveProvisionedDashboard") defer span.End() @@ -580,6 +630,7 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, {SQL: "DELETE FROM dashboard_version WHERE dashboard_id = ?", args: []any{dashboard.ID}}, {SQL: "DELETE FROM dashboard_provisioning WHERE dashboard_id = ?", args: []any{dashboard.ID}}, {SQL: "DELETE FROM dashboard_acl WHERE dashboard_id = ?", args: []any{dashboard.ID}}, + {SQL: "DELETE FROM library_element_connection WHERE connection_id = ?", args: []any{dashboard.ID}}, } if dashboard.IsFolder { diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 24b414118f3..3c661ee6b9c 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -4,6 +4,9 @@ import ( "fmt" "time" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" @@ -95,6 +98,11 @@ func (d *Dashboard) GetTags() []string { } func NewDashboardFromJson(data *simplejson.Json) *Dashboard { + // if apiVersion is set in the json - use it as an indicator that it is in the k8s format + if apiVersion, err := data.Get("apiVersion").String(); err == nil && apiVersion != "" { + return parseK8sDashboard(data) + } + dash := &Dashboard{} dash.Data = data dash.Title = dash.Data.Get("title").MustString() @@ -127,6 +135,66 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { return dash } +// parses json in the k8s format +// i.e: +// +// { +// "apiVersion": "dashboard.grafana.app/v1", +// "kind": "Dashboard", +// "metadata": {...}, +// "spec": {...} +// } +func parseK8sDashboard(data *simplejson.Json) *Dashboard { + dash := &Dashboard{} + + dataMap, ok := data.Interface().(map[string]interface{}) + if !ok { + return dash + } + + item := &unstructured.Unstructured{Object: dataMap} + obj, err := utils.MetaAccessor(item) + if err != nil { + return dash + } + dash.APIVersion = item.GetAPIVersion() + info, err := types.ParseNamespace(obj.GetNamespace()) + if err == nil && info.OrgID > 0 { + dash.OrgID = info.OrgID + } + dash.UID = obj.GetName() + + spec, ok := item.Object["spec"].(map[string]any) + if !ok { + return dash + } + dash.Data = simplejson.NewFromAny(spec) + + dash.Title = obj.FindTitle("") + dash.UpdateSlug() + + dash.FolderUID = obj.GetFolder() + dash.Data.Set("uid", dash.UID) + + generation := obj.GetGeneration() + if generation > 0 { + dash.Data.Set("version", generation) + dash.Updated = time.Now() + } else { + dash.Data.Set("version", 0) + dash.Created = time.Now() + dash.Updated = time.Now() + } + + dash.Data.Set("id", obj.GetDeprecatedInternalID()) // nolint:staticcheck + + if gnetId, err := dash.Data.Get("gnetId").Float64(); err == nil { + dash.GnetID = int64(gnetId) + } + + return dash +} + // GetDashboardModel turns the command into the saveable model func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash := NewDashboardFromJson(cmd.Dashboard) @@ -241,11 +309,15 @@ type DeleteOrphanedProvisionedDashboardsCommand struct { } type DashboardProvisioningSearchResults struct { - Dashboard Dashboard `xorm:"extends"` - Provisioner string `xorm:"name"` - ExternalID string `xorm:"external_id"` - CheckSum string `xorm:"check_sum"` - ProvisionUpdate int64 `xorm:"provisioning_updated"` + ID int64 `xorm:"id"` + UID string `xorm:"uid"` + Title string `xorm:"title"` + FolderUID string `xorm:"folder_uid"` + OrgID int64 `xorm:"org_id"` + Provisioner string `xorm:"name"` + ExternalID string `xorm:"external_id"` + CheckSum string `xorm:"check_sum"` + ProvisionUpdate int64 `xorm:"provisioning_updated"` } // diff --git a/pkg/services/dashboards/models_test.go b/pkg/services/dashboards/models_test.go index 06aeb1eba60..0a3035cbecc 100644 --- a/pkg/services/dashboards/models_test.go +++ b/pkg/services/dashboards/models_test.go @@ -86,3 +86,50 @@ func TestSlugifyTitle(t *testing.T) { }) } } + +func TestParseK8sDashboard(t *testing.T) { + t.Run("should parse valid K8s dashboard with all fields", func(t *testing.T) { + data := simplejson.NewFromAny(map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "test-dashboard-uid", + "namespace": "org-123", + "generation": int64(5), + "labels": map[string]interface{}{ + "grafana.app/deprecatedInternalID": "456", + }, + "annotations": map[string]interface{}{ + "grafana.app/folder": "test-folder-uid", + }, + }, + "spec": map[string]interface{}{ + "title": "Test Dashboard", + "gnetId": float64(12345), + }, + }) + + dash := parseK8sDashboard(data) + assert.Equal(t, "dashboard.grafana.app/v2alpha1", dash.APIVersion) + assert.Equal(t, int64(123), dash.OrgID) + assert.Equal(t, "test-dashboard-uid", dash.UID) + assert.Equal(t, "Test Dashboard", dash.Title) + assert.Equal(t, "test-dashboard", dash.Slug) + assert.Equal(t, "test-folder-uid", dash.FolderUID) + assert.Equal(t, int64(12345), dash.GnetID) + assert.Equal(t, "test-dashboard-uid", dash.Data.Get("uid").MustString()) + assert.Equal(t, int64(5), dash.Data.Get("version").MustInt64()) + assert.Equal(t, int64(456), dash.Data.Get("id").MustInt64()) + assert.False(t, dash.Updated.IsZero()) + assert.True(t, dash.Created.IsZero()) + }) + + t.Run("should handle invalid input (not a map)", func(t *testing.T) { + data := simplejson.NewFromAny("invalid string") + dash := parseK8sDashboard(data) + assert.Empty(t, dash.UID) + assert.Empty(t, dash.Title) // this will fail later in the provisioning chain because its empty + assert.Empty(t, dash.APIVersion) + assert.Nil(t, dash.Data) + }) +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 3d26169edd7..9e6baf6b8aa 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -676,11 +676,14 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d // Validate folder if dash.FolderID != 0 || dash.FolderUID != "" { // nolint:staticcheck - folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{ + // use a background requester, because the user may have been granted edit access to that specific dashboard, but + // not have access to the parent folder. we should still allow editing in that case. + ctxIdentity, backgroundRequester := identity.WithServiceIdentity(ctx, dash.OrgID) + folder, err := dr.folderService.Get(ctxIdentity, &folder.GetFolderQuery{ OrgID: dash.OrgID, UID: &dash.FolderUID, ID: &dash.FolderID, // nolint:staticcheck - SignedInUser: dto.User, + SignedInUser: backgroundRequester, }) if err != nil { return nil, err @@ -742,15 +745,16 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() cmd := &dashboards.SaveDashboardCommand{ - Dashboard: dash.Data, - Message: dto.Message, - OrgID: dto.OrgID, - Overwrite: dto.Overwrite, - UserID: userID, - FolderID: dash.FolderID, // nolint:staticcheck - FolderUID: dash.FolderUID, - IsFolder: dash.IsFolder, - PluginID: dash.PluginID, + Dashboard: dash.Data, + Message: dto.Message, + OrgID: dto.OrgID, + Overwrite: dto.Overwrite, + UserID: userID, + FolderID: dash.FolderID, // nolint:staticcheck + FolderUID: dash.FolderUID, + IsFolder: dash.IsFolder, + PluginID: dash.PluginID, + APIVersion: dash.APIVersion, } if !dto.UpdatedAt.IsZero() { @@ -873,6 +877,12 @@ func (dr *DashboardServiceImpl) waitForSearchQuery(ctx context.Context, query *d } func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error { + // cleanup duplicate provisioned dashboards first (this will have the same name and external_id) + // note: only works in modes 1-3 + if err := dr.DeleteDuplicateProvisionedDashboards(ctx); err != nil { + dr.log.Error("Failed to delete duplicate provisioned dashboards", "error", err) + } + // check each org for orphaned provisioned dashboards orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{}) if err != nil { @@ -911,6 +921,34 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. return nil } +func (dr *DashboardServiceImpl) DeleteDuplicateProvisionedDashboards(ctx context.Context) error { + duplicates, err := dr.dashboardStore.GetDuplicateProvisionedDashboards(ctx) + if err != nil { + return err + } + + type provisioningKey struct { + name string + externalID string + } + + groups := make(map[provisioningKey][]*dashboards.DashboardProvisioningSearchResults) + for _, dash := range duplicates { + key := provisioningKey{ + name: dash.Provisioner, + externalID: dash.ExternalID, + } + if _, exists := groups[key]; exists { + if err = dr.deleteDashboard(ctx, dash.ID, dash.UID, dash.OrgID, false); err != nil { + dr.log.Error("Failed to delete duplicate provisioned dashboard", "error", err, "dashboardUID", dash.UID, "dashboardID", dash.ID) + } + } + groups[key] = append(groups[key], dash) + } + + return nil +} + func (dr *DashboardServiceImpl) ValidateDashboardRefreshInterval(minRefreshInterval string, targetRefreshInterval string) error { if minRefreshInterval == "" { return nil @@ -1174,12 +1212,7 @@ func (dr *DashboardServiceImpl) SetDefaultPermissionsAfterCreate(ctx context.Con if err != nil { return err } - uid, err := user.GetInternalID() - if err != nil { - return err - } permissions := []accesscontrol.SetResourcePermissionCommand{} - isNested := obj.GetFolder() != "" //nolint:staticcheck // not yet migrated to OpenFeature if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) && isNested { @@ -1187,6 +1220,10 @@ func (dr *DashboardServiceImpl) SetDefaultPermissionsAfterCreate(ctx context.Con return nil } if user.IsIdentityType(claims.TypeUser, claims.TypeServiceAccount) { + uid, err := user.GetInternalID() + if err != nil { + return err + } permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ UserID: uid, Permission: dashboardaccess.PERMISSION_ADMIN.String(), }) @@ -1638,12 +1675,14 @@ func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64 defer span.End() // We need a list of dashboard uids inside the folder to delete related public dashboards - dashes, err := dr.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{ + dashes, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ SignedInUser: u, - FolderUIDs: folderUIDs, - OrgId: orgID, Type: searchstore.TypeDashboard, + Limit: 100000, + OrgId: orgID, + FolderUIDs: folderUIDs, }) + if err != nil { return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err) } @@ -1658,7 +1697,14 @@ func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64 return err } - return dr.dashboardStore.DeleteDashboardsInFolders(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID}) + for _, dash := range dashes { + errDel := dr.DeleteDashboard(ctx, dash.ID, dash.UID, orgID) + if errDel != nil { + dr.log.Error("failed to delete dashboard inside folder", "dashboardUID", dash.UID, "folderUIDs", folderUIDs, "error", errDel) + } + } + + return err } func (dr *DashboardServiceImpl) Kind() string { return entity.StandardKindDashboard } @@ -2243,6 +2289,10 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names meta.SetMessage(cmd.Message) } + if cmd.APIVersion != "" { + finalObj.SetAPIVersion(cmd.APIVersion) + } + return finalObj, nil } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index e5043dff640..7aa0e07762c 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -629,16 +629,19 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) { func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) + fakeDashboardStore := &dashboards.FakeDashboardStore{} service := &DashboardServiceImpl{ cfg: setting.NewCfg(), orgService: &orgtest.FakeOrgService{ ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 2}}, }, publicDashboardService: fakePublicDashboardService, + dashboardStore: fakeDashboardStore, log: log.NewNopLogger(), } t.Run("Should delete across all orgs, but only delete file based provisioned dashboards", func(t *testing.T) { + fakeDashboardStore.On("GetDuplicateProvisionedDashboards", mock.Anything).Return([]*dashboards.DashboardProvisioningSearchResults{}, nil) _, k8sCliMock := setupK8sDashboardTests(service) k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) @@ -783,6 +786,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { }) t.Run("Should retry until deleted dashboard not found in search", func(t *testing.T) { + fakeDashboardStore.On("GetDuplicateProvisionedDashboards", mock.Anything).Return([]*dashboards.DashboardProvisioningSearchResults{}, nil) repo := "test" singleOrgService := &DashboardServiceImpl{ cfg: setting.NewCfg(), @@ -790,6 +794,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, }, publicDashboardService: fakePublicDashboardService, + dashboardStore: fakeDashboardStore, log: log.NewNopLogger(), } ctx, k8sCliMock := setupK8sDashboardTests(singleOrgService) @@ -876,6 +881,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { }) t.Run("Will not wait for indexer when no dashboards were deleted", func(t *testing.T) { + fakeDashboardStore.On("GetDuplicateProvisionedDashboards", mock.Anything).Return([]*dashboards.DashboardProvisioningSearchResults{}, nil) repo := "test" singleOrgService := &DashboardServiceImpl{ cfg: setting.NewCfg(), @@ -883,6 +889,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, }, publicDashboardService: fakePublicDashboardService, + dashboardStore: fakeDashboardStore, log: log.NewNopLogger(), } ctx, k8sCliMock := setupK8sDashboardTests(singleOrgService) @@ -2127,6 +2134,7 @@ func TestSetDefaultPermissionsAfterCreate(t *testing.T) { name string rootFolder bool featureKubernetesDashboards bool + User *user.SignedInUser expectedPermission []accesscontrol.SetResourcePermissionCommand }{ { @@ -2149,6 +2157,19 @@ func TestSetDefaultPermissionsAfterCreate(t *testing.T) { {BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()}, }, }, + { + name: "with kubernetesDashboards feature in root folder and user is anonymous", + rootFolder: true, + featureKubernetesDashboards: true, + User: &user.SignedInUser{ + IsAnonymous: true, + UserID: 0, + }, + expectedPermission: []accesscontrol.SetResourcePermissionCommand{ + {BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()}, + {BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()}, + }, + }, { name: "without kubernetesDashboards feature in subfolder", rootFolder: false, @@ -2172,6 +2193,9 @@ func TestSetDefaultPermissionsAfterCreate(t *testing.T) { OrgRole: "Admin", UserID: 1, } + if tc.User != nil { + user = tc.User + } ctx := request.WithNamespace(context.Background(), "default") ctx = identity.WithRequester(ctx, user) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 4a3e0d15d90..6a329e10301 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -305,6 +305,36 @@ func (_m *FakeDashboardStore) GetOrphanedProvisionedDashboards(ctx context.Conte return r0, r1 } +// GetDuplicateProvisionedDashboards provides a mock function with given fields: ctx +func (_m *FakeDashboardStore) GetDuplicateProvisionedDashboards(ctx context.Context) ([]*DashboardProvisioningSearchResults, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetDuplicateProvisionedDashboards") + } + + var r0 []*DashboardProvisioningSearchResults + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*DashboardProvisioningSearchResults, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*DashboardProvisioningSearchResults); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardProvisioningSearchResults) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetProvisionedDashboardData provides a mock function with given fields: ctx, name func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) { ret := _m.Called(ctx, name) diff --git a/pkg/services/datasources/datasources.go b/pkg/services/datasources/datasources.go index 5b45fd4d96a..a04f35e3775 100644 --- a/pkg/services/datasources/datasources.go +++ b/pkg/services/datasources/datasources.go @@ -15,6 +15,9 @@ type DataSourceService interface { // GetDataSource gets a datasource. GetDataSource(ctx context.Context, query *GetDataSourceQuery) (*DataSource, error) + // GetDataSourceInNamespace gets a datasource by namespace, name (datasource uid), and group (datasource type). + GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*DataSource, error) + // GetDataSources gets datasources. GetDataSources(ctx context.Context, query *GetDataSourcesQuery) ([]*DataSource, error) diff --git a/pkg/services/datasources/fakes/fake_datasource_service.go b/pkg/services/datasources/fakes/fake_datasource_service.go index 4c5fd79173f..19e7bbb43a5 100644 --- a/pkg/services/datasources/fakes/fake_datasource_service.go +++ b/pkg/services/datasources/fakes/fake_datasource_service.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/grafana/authlib/types" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/infra/httpclient" @@ -30,6 +31,19 @@ func (s *FakeDataSourceService) GetDataSource(ctx context.Context, query *dataso return nil, datasources.ErrDataSourceNotFound } +func (s *FakeDataSourceService) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) { + ns, err := types.ParseNamespace(namespace) + if err != nil { + return nil, err + } + for _, dataSource := range s.DataSources { + if name == dataSource.UID && ns.OrgID == dataSource.OrgID && group == dataSource.Type { + return dataSource, nil + } + } + return nil, datasources.ErrDataSourceNotFound +} + func (s *FakeDataSourceService) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) { var dataSources []*datasources.DataSource for _, datasource := range s.DataSources { diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index f5cbe96225b..84a3e14141c 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -117,6 +117,8 @@ func (s *Service) Usage(ctx context.Context, scopeParams *quota.ScopeParameters) type DataSourceRetriever interface { // GetDataSource gets a datasource. GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) + // GetDataSourceInNamespace gets a datasource by namespace, name (datasource uid), and group (datasource type). + GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) } // NewNameScopeResolver provides an ScopeAttributeResolver able to @@ -176,6 +178,10 @@ func (s *Service) GetDataSource(ctx context.Context, query *datasources.GetDataS return s.SQLStore.GetDataSource(ctx, query) } +func (s *Service) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) { + return s.SQLStore.GetDataSourceInNamespace(ctx, namespace, name, group) +} + func (s *Service) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) { return s.SQLStore.GetDataSources(ctx, query) } diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go index 0de33c32e5f..1aeca6af087 100644 --- a/pkg/services/datasources/service/datasource_test.go +++ b/pkg/services/datasources/service/datasource_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/ini.v1" + "github.com/grafana/authlib/types" "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/components/simplejson" @@ -63,6 +64,19 @@ func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *data return nil, datasources.ErrDataSourceNotFound } +func (d *dataSourceMockRetriever) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) { + ns, err := types.ParseNamespace(namespace) + if err != nil { + return nil, err + } + for _, dataSource := range d.res { + if name == dataSource.UID && ns.OrgID == dataSource.OrgID && group == dataSource.Type { + return dataSource, nil + } + } + return nil, datasources.ErrDataSourceNotFound +} + func TestIntegrationService_AddDataSource(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) diff --git a/pkg/services/datasources/service/store.go b/pkg/services/datasources/service/store.go index c89200ee2fc..e0ef6a88111 100644 --- a/pkg/services/datasources/service/store.go +++ b/pkg/services/datasources/service/store.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/util/xorm" "github.com/grafana/grafana/pkg/components/simplejson" @@ -24,6 +25,7 @@ import ( // Store is the interface for the datasource Service's storage. type Store interface { GetDataSource(context.Context, *datasources.GetDataSourceQuery) (*datasources.DataSource, error) + GetDataSourceInNamespace(context.Context, string, string, string) (*datasources.DataSource, error) GetDataSources(context.Context, *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) GetDataSourcesByType(context.Context, *datasources.GetDataSourcesByTypeQuery) ([]*datasources.DataSource, error) DeleteDataSource(context.Context, *datasources.DeleteDataSourceCommand) error @@ -90,6 +92,41 @@ func (ss *SqlStore) getDataSource(_ context.Context, query *datasources.GetDataS return datasource, nil } +func (ss *SqlStore) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) { + var ( + dataSource *datasources.DataSource + err error + ) + ns, err := types.ParseNamespace(namespace) + if err != nil { + return nil, err + } + + return dataSource, ss.db.WithDbSession(ctx, func(sess *db.Session) error { + dataSource, err = ss.getDataSourceInGroup(ctx, ns.OrgID, name, group, sess) + return err + }) +} + +func (ss *SqlStore) getDataSourceInGroup(_ context.Context, orgID int64, name, group string, sess *db.Session) (*datasources.DataSource, error) { + datasource := &datasources.DataSource{ + OrgID: orgID, + Type: group, + UID: name, + } + has, err := sess.Get(datasource) + + if err != nil { + ss.logger.Error("Failed getting data source", "err", err, "name", name, "orgId", orgID, "group", group) + return nil, err + } else if !has { + ss.logger.Debug("Data source not found", "name", name, "orgId", orgID, "group", group) + return nil, datasources.ErrDataSourceNotFound + } + + return datasource, nil +} + func (ss *SqlStore) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) { var ( sess *xorm.Session diff --git a/pkg/services/datasources/service/store_test.go b/pkg/services/datasources/service/store_test.go index eef123b138b..2de184c5c62 100644 --- a/pkg/services/datasources/service/store_test.go +++ b/pkg/services/datasources/service/store_test.go @@ -407,6 +407,43 @@ func TestIntegrationDataAccess(t *testing.T) { }) }) + t.Run("GetDataSourceInGroup", func(t *testing.T) { + t.Run("Only returns datasource of specified type", func(t *testing.T) { + db := db.InitTestDB(t) + ss := SqlStore{db: db, logger: log.NewNopLogger()} + + ds, err := ss.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + OrgID: 10, + Name: "Elasticsearch", + Type: datasources.DS_ES, + Access: datasources.DS_ACCESS_DIRECT, + URL: "http://test", + Database: "site", + ReadOnly: true, + }) + require.NoError(t, err) + + ds2, err := ss.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + OrgID: 10, + Name: "Graphite", + Type: datasources.DS_GRAPHITE, + Access: datasources.DS_ACCESS_DIRECT, + URL: "http://test", + Database: "site", + ReadOnly: true, + }) + require.NoError(t, err) + + dataSource, err := ss.GetDataSourceInNamespace(context.Background(), "org-10", ds.UID, datasources.DS_ES) + require.NoError(t, err) + require.Equal(t, ds.UID, dataSource.UID) + + _, err = ss.GetDataSourceInNamespace(context.Background(), "org-10", ds2.UID, datasources.DS_ES) + require.Error(t, err) + require.IsType(t, datasources.ErrDataSourceNotFound, err) + }) + }) + t.Run("GetDataSourcesByType", func(t *testing.T) { t.Run("Only returns datasources of specified type", func(t *testing.T) { db := db.InitTestDB(t) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 491c1142b3b..cf7202d4120 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -73,7 +73,7 @@ var ( Name: "correlations", Description: "Correlations page", Stage: FeatureStageGeneralAvailability, - Owner: grafanaDatavizSquad, + Owner: grafanaDataProSquad, Expression: "true", // enabled by default AllowSelfServe: true, }, @@ -219,7 +219,7 @@ var ( Name: "disableSSEDataplane", Description: "Disables dataplane specific processing in server side expressions.", Stage: FeatureStageExperimental, - Owner: grafanaObservabilityMetricsSquad, + Owner: grafanaDatasourcesCoreServicesSquad, }, { Name: "unifiedRequestLog", @@ -264,20 +264,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaFrontendPlatformSquad, }, - { - Name: "pluginsFrontendSandbox", - Description: "Enables the plugins frontend sandbox", - Stage: FeatureStagePrivatePreview, - Owner: grafanaPluginsPlatformSquad, - }, - { - Name: "recordedQueriesMulti", - Description: "Enables writing multiple items from a single query within Recorded Queries", - Stage: FeatureStageGeneralAvailability, - Expression: "true", - Owner: grafanaObservabilityMetricsSquad, - AllowSelfServe: false, - }, { Name: "logsExploreTableVisualisation", Description: "A table visualisation for logs in Explore", @@ -395,7 +381,7 @@ var ( Name: "sseGroupByDatasource", Description: "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.", Stage: FeatureStageExperimental, - Owner: grafanaObservabilityMetricsSquad, + Owner: grafanaDatasourcesCoreServicesSquad, }, { Name: "lokiRunQueriesInParallel", @@ -411,14 +397,6 @@ var ( Stage: FeatureStagePublicPreview, Owner: identityAccessTeam, }, - { - Name: "panelMonitoring", - Description: "Enables panel monitoring through logs and measurements", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: grafanaDatavizSquad, - FrontendOnly: true, - }, { Name: "enableNativeHTTPHistogram", Description: "Enables native HTTP Histograms", @@ -444,7 +422,7 @@ var ( Description: "Enable format string transformer", Stage: FeatureStageGeneralAvailability, FrontendOnly: true, - Owner: grafanaDatavizSquad, + Owner: grafanaDataProSquad, Expression: "true", // enabled by default }, { @@ -595,7 +573,7 @@ var ( Description: "Add cumulative and window functions to the add field from calculation transformation", Stage: FeatureStageGeneralAvailability, FrontendOnly: true, - Owner: grafanaDatavizSquad, + Owner: grafanaDataProSquad, Expression: "true", // enabled by default }, { @@ -626,13 +604,6 @@ var ( Expression: "true", // enabled by default Owner: identityAccessTeam, }, - { - Name: "extractFieldsNameDeduplication", - Description: "Make sure extracted field names are unique in the dataframe", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaDatavizSquad, - }, { Name: "dashboardSceneForViewers", Description: "Enables dashboard rendering using Scenes for viewer roles", @@ -742,7 +713,7 @@ var ( Description: "Enables regression analysis transformation", Stage: FeatureStagePublicPreview, FrontendOnly: true, - Owner: grafanaDatavizSquad, + Owner: grafanaDataProSquad, }, { Name: "kubernetesFeatureToggles", @@ -872,7 +843,7 @@ var ( Description: "Enables the group to nested table transformation", Stage: FeatureStageGeneralAvailability, FrontendOnly: true, - Owner: grafanaDatavizSquad, + Owner: grafanaDataProSquad, Expression: "true", // enabled by default, }, { @@ -977,6 +948,13 @@ var ( }, { Name: "dashboardLibrary", + Description: "Enable dashboard library experiments that are production ready", + Stage: FeatureStageExperimental, + Owner: grafanaSharingSquad, + FrontendOnly: false, + }, + { + Name: "suggestedDashboards", Description: "Enable suggested dashboards when creating new dashboards", Stage: FeatureStageExperimental, Owner: grafanaSharingSquad, @@ -1254,6 +1232,13 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaFrontendPlatformSquad, }, + { + Name: "timeRangePan", + Description: "Enables time range panning functionality", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDatavizSquad, + }, { Name: "azureMonitorDisableLogLimit", Description: "Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.", @@ -1261,13 +1246,6 @@ var ( Owner: grafanaPartnerPluginsSquad, Expression: "false", }, - { - Name: "preinstallAutoUpdate", - Description: "Enables automatic updates for pre-installed plugins", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaPluginsPlatformSquad, - Expression: "true", // enabled by default - }, { Name: "playlistsReconciler", Description: "Enables experimental reconciler for playlists", @@ -1516,10 +1494,11 @@ var ( }, { Name: "newLogsPanel", - Description: "Enables the new logs panel in Explore", - Stage: FeatureStageExperimental, + Description: "Enables the new logs panel", + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, + Expression: "true", }, { Name: "grafanaconThemes", @@ -1867,10 +1846,10 @@ var ( { Name: "sharingDashboardImage", Description: "Enables image sharing functionality for dashboards", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, Owner: grafanaSharingSquad, - HideFromDocs: true, FrontendOnly: true, + Expression: "true", }, { Name: "preferLibraryPanelTitle", @@ -1948,14 +1927,6 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, - { - Name: "dashboardDsAdHocFiltering", - Description: "Enables adhoc filtering support for the dashboard datasource", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaDataProSquad, - FrontendOnly: true, - Expression: "true", - }, { Name: "dashboardLevelTimeMacros", Description: "Supports __from and __to macros that always use the dashboard level time range", @@ -1981,14 +1952,6 @@ var ( FrontendOnly: true, Expression: "false", }, - { - Name: "adhocFiltersInTooltips", - Description: "Enable adhoc filter buttons in visualization tooltips", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaDataProSquad, - FrontendOnly: true, - Expression: "true", - }, { Name: "favoriteDatasources", Description: "Enable favorite datasources", @@ -2115,6 +2078,12 @@ var ( Owner: grafanaFrontendPlatformSquad, Expression: "true", }, + { + Name: "jaegerEnableGrpcEndpoint", + Description: "Enable querying trace data through Jaeger's gRPC endpoint (HTTP)", + Stage: FeatureStageExperimental, + Owner: grafanaOSSBigTent, + }, { Name: "pluginStoreServiceLoading", Description: "Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded", @@ -2133,6 +2102,31 @@ var ( Owner: identityAccessTeam, Expression: "true", }, + { + Name: "panelTimeSettings", + Description: "Enables a new panel time settings drawer", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + AllowSelfServe: false, + HideFromDocs: false, + HideFromAdminPage: false, + }, + { + Name: "dashboardTemplates", + Description: "Enable template dashboards", + Stage: FeatureStageExperimental, + Owner: grafanaSharingSquad, + FrontendOnly: false, + }, + { + Name: "kubernetesAnnotations", + Description: "Enables app platform API for annotations", + Stage: FeatureStageExperimental, + Owner: grafanaBackendServicesSquad, + Expression: "false", + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4b64ee08612..317eff8c6bc 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -6,7 +6,7 @@ publicDashboardsScene,GA,@grafana/grafana-operator-experience-squad,false,false, lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,false featureHighlights,GA,@grafana/grafana-operator-experience-squad,false,false,false storage,experimental,@grafana/search-and-storage,false,false,false -correlations,GA,@grafana/dataviz-squad,false,false,false +correlations,GA,@grafana/datapro,false,false,false canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true logRequestsInstrumentedAsUnknown,experimental,@grafana/grafana-backend-group,false,false,false grpcServer,preview,@grafana/search-and-storage,false,false,false @@ -26,15 +26,13 @@ influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,fa influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false dataplaneFrontendFallback,GA,@grafana/observability-metrics,false,false,true -disableSSEDataplane,experimental,@grafana/observability-metrics,false,false,false +disableSSEDataplane,experimental,@grafana/grafana-datasources-core-services,false,false,false unifiedRequestLog,GA,@grafana/grafana-backend-group,false,false,false renderAuthJWT,preview,@grafana/grafana-operator-experience-squad,false,false,false refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true -pluginsFrontendSandbox,privatePreview,@grafana/plugins-platform-backend,false,false,false -recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true awsDatasourcesTempCredentials,GA,@grafana/aws-datasources,false,false,false transformationsRedesign,GA,@grafana/observability-metrics,false,false,true @@ -50,13 +48,12 @@ configurableSchedulerTick,experimental,@grafana/alerting-squad,false,true,false dashgpt,GA,@grafana/dashboards-squad,false,false,true aiGeneratedDashboardChanges,experimental,@grafana/dashboards-squad,false,false,true reportingRetries,preview,@grafana/grafana-operator-experience-squad,false,true,false -sseGroupByDatasource,experimental,@grafana/observability-metrics,false,false,false +sseGroupByDatasource,experimental,@grafana/grafana-datasources-core-services,false,false,false lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false -panelMonitoring,GA,@grafana/dataviz-squad,false,false,true enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false -formatString,GA,@grafana/dataviz-squad,false,false,true +formatString,GA,@grafana/datapro,false,false,true kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesDashboards,GA,@grafana/dashboards-squad,false,false,true @@ -78,12 +75,11 @@ queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false queryServiceFromExplore,experimental,@grafana/grafana-datasources-core-services,false,false,true cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false -addFieldFromCalculationStatFunctions,GA,@grafana/dataviz-squad,false,false,true +addFieldFromCalculationStatFunctions,GA,@grafana/datapro,false,false,true alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false alertingProvenanceLockWrites,experimental,@grafana/alerting-squad,false,false,false alertmanagerRemotePrimary,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,GA,@grafana/dashboards-squad,false,false,true dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true dashboardScene,GA,@grafana/dashboards-squad,false,false,true @@ -98,7 +94,7 @@ logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true -regressionTransformation,preview,@grafana/dataviz-squad,false,false,true +regressionTransformation,preview,@grafana/datapro,false,false,true kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false @@ -114,7 +110,7 @@ useMultipleScopeNodesEndpoint,experimental,@grafana/grafana-operator-experience- logQLScope,privatePreview,@grafana/observability-logs,false,false,false sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,false sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true -groupToNestedTableTransformation,GA,@grafana/dataviz-squad,false,false,true +groupToNestedTableTransformation,GA,@grafana/datapro,false,false,true newPDFRendering,GA,@grafana/grafana-operator-experience-squad,false,false,false tlsMemcached,GA,@grafana/grafana-operator-experience-squad,false,false,false kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false @@ -128,6 +124,7 @@ disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,fal grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false queryLibrary,preview,@grafana/sharing-squad,false,false,false dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false +suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false @@ -164,8 +161,8 @@ managedDualWriter,experimental,@grafana/search-and-storage,false,false,false pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false +timeRangePan,experimental,@grafana/dataviz-squad,false,false,true azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false -preinstallAutoUpdate,GA,@grafana/plugins-platform-backend,false,false,false playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true @@ -198,7 +195,7 @@ grafanaAdvisor,privatePreview,@grafana/plugins-platform-backend,false,false,fals elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false datasourceConnectionsTab,privatePreview,@grafana/plugins-platform-backend,false,false,true fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false -newLogsPanel,experimental,@grafana/observability-logs,false,false,true +newLogsPanel,GA,@grafana/observability-logs,false,false,true grafanaconThemes,GA,@grafana/grafana-frontend-platform,false,true,false alertingJiraIntegration,experimental,@grafana/alerting-squad,false,false,true alertingUseNewSimplifiedRoutingHashAlgorithm,preview,@grafana/alerting-squad,false,true,false @@ -241,7 +238,7 @@ alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false alertingImportAlertmanagerAPI,experimental,@grafana/alerting-squad,false,false,false alertingImportAlertmanagerUI,experimental,@grafana/alerting-squad,false,false,false -sharingDashboardImage,experimental,@grafana/sharing-squad,false,false,true +sharingDashboardImage,GA,@grafana/sharing-squad,false,false,true preferLibraryPanelTitle,privatePreview,@grafana/dashboards-squad,false,false,false tabularNumbers,GA,@grafana/grafana-frontend-platform,false,false,false newInfluxDSConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false @@ -251,11 +248,9 @@ foldersAppPlatformAPI,experimental,@grafana/grafana-search-navigate-organise,fal otelLogsFormatting,experimental,@grafana/observability-logs,false,false,true alertingNotificationHistory,experimental,@grafana/alerting-squad,false,false,false unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,false,false,false -dashboardDsAdHocFiltering,GA,@grafana/datapro,false,false,true dashboardLevelTimeMacros,experimental,@grafana/dashboards-squad,false,false,true alertmanagerRemoteSecondaryWithRemoteState,experimental,@grafana/alerting-squad,false,false,false restrictedPluginApis,experimental,@grafana/plugins-platform-backend,false,false,true -adhocFiltersInTooltips,GA,@grafana/datapro,false,false,true favoriteDatasources,experimental,@grafana/plugins-platform-backend,false,false,true newLogContext,experimental,@grafana/observability-logs,false,false,true newClickhouseConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false @@ -272,5 +267,9 @@ cdnPluginsUrls,experimental,@grafana/plugins-platform-backend,false,false,false pluginInstallAPISync,experimental,@grafana/plugins-platform-backend,false,false,false newGauge,experimental,@grafana/dataviz-squad,false,false,true preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true +jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false +panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false +dashboardTemplates,experimental,@grafana/sharing-squad,false,false,false +kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 8b28acc992b..758d42e01ef 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -143,14 +143,6 @@ const ( // Enables extra themes FlagExtraThemes = "extraThemes" - // FlagPluginsFrontendSandbox - // Enables the plugins frontend sandbox - FlagPluginsFrontendSandbox = "pluginsFrontendSandbox" - - // FlagRecordedQueriesMulti - // Enables writing multiple items from a single query within Recorded Queries - FlagRecordedQueriesMulti = "recordedQueriesMulti" - // FlagLogsExploreTableVisualisation // A table visualisation for logs in Explore FlagLogsExploreTableVisualisation = "logsExploreTableVisualisation" @@ -223,10 +215,6 @@ const ( // Automatic service account and token setup for plugins FlagExternalServiceAccounts = "externalServiceAccounts" - // FlagPanelMonitoring - // Enables panel monitoring through logs and measurements - FlagPanelMonitoring = "panelMonitoring" - // FlagEnableNativeHTTPHistogram // Enables native HTTP Histograms FlagEnableNativeHTTPHistogram = "enableNativeHTTPHistogram" @@ -343,10 +331,6 @@ const ( // Change the way annotation permissions work by scoping them to folders and dashboards. FlagAnnotationPermissionUpdate = "annotationPermissionUpdate" - // FlagExtractFieldsNameDeduplication - // Make sure extracted field names are unique in the dataframe - FlagExtractFieldsNameDeduplication = "extractFieldsNameDeduplication" - // FlagDashboardSceneForViewers // Enables dashboard rendering using Scenes for viewer roles FlagDashboardSceneForViewers = "dashboardSceneForViewers" @@ -520,9 +504,13 @@ const ( FlagQueryLibrary = "queryLibrary" // FlagDashboardLibrary - // Enable suggested dashboards when creating new dashboards + // Enable dashboard library experiments that are production ready FlagDashboardLibrary = "dashboardLibrary" + // FlagSuggestedDashboards + // Enable suggested dashboards when creating new dashboards + FlagSuggestedDashboards = "suggestedDashboards" + // FlagLogsExploreTableDefaultVisualization // Sets the logs table as default visualisation in logs explore FlagLogsExploreTableDefaultVisualization = "logsExploreTableDefaultVisualization" @@ -667,14 +655,14 @@ const ( // Enables time pickers sync FlagTimeRangeProvider = "timeRangeProvider" + // FlagTimeRangePan + // Enables time range panning functionality + FlagTimeRangePan = "timeRangePan" + // FlagAzureMonitorDisableLogLimit // Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. FlagAzureMonitorDisableLogLimit = "azureMonitorDisableLogLimit" - // FlagPreinstallAutoUpdate - // Enables automatic updates for pre-installed plugins - FlagPreinstallAutoUpdate = "preinstallAutoUpdate" - // FlagPlaylistsReconciler // Enables experimental reconciler for playlists FlagPlaylistsReconciler = "playlistsReconciler" @@ -804,7 +792,7 @@ const ( FlagFetchRulesUsingPost = "fetchRulesUsingPost" // FlagNewLogsPanel - // Enables the new logs panel in Explore + // Enables the new logs panel FlagNewLogsPanel = "newLogsPanel" // FlagGrafanaconThemes @@ -1014,10 +1002,6 @@ const ( // Enable dual reader for unified storage search FlagUnifiedStorageSearchDualReaderEnabled = "unifiedStorageSearchDualReaderEnabled" - // FlagDashboardDsAdHocFiltering - // Enables adhoc filtering support for the dashboard datasource - FlagDashboardDsAdHocFiltering = "dashboardDsAdHocFiltering" - // FlagDashboardLevelTimeMacros // Supports __from and __to macros that always use the dashboard level time range FlagDashboardLevelTimeMacros = "dashboardLevelTimeMacros" @@ -1030,10 +1014,6 @@ const ( // Enables sharing a list of APIs with a list of plugins FlagRestrictedPluginApis = "restrictedPluginApis" - // FlagAdhocFiltersInTooltips - // Enable adhoc filter buttons in visualization tooltips - FlagAdhocFiltersInTooltips = "adhocFiltersInTooltips" - // FlagFavoriteDatasources // Enable favorite datasources FlagFavoriteDatasources = "favoriteDatasources" @@ -1098,6 +1078,10 @@ const ( // Restrict PanelChrome contents with overflow: hidden; FlagPreventPanelChromeOverflow = "preventPanelChromeOverflow" + // FlagJaegerEnableGrpcEndpoint + // Enable querying trace data through Jaeger's gRPC endpoint (HTTP) + FlagJaegerEnableGrpcEndpoint = "jaegerEnableGrpcEndpoint" + // FlagPluginStoreServiceLoading // Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded FlagPluginStoreServiceLoading = "pluginStoreServiceLoading" @@ -1105,4 +1089,16 @@ const ( // FlagOnlyStoreActionSets // When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission FlagOnlyStoreActionSets = "onlyStoreActionSets" + + // FlagPanelTimeSettings + // Enables a new panel time settings drawer + FlagPanelTimeSettings = "panelTimeSettings" + + // FlagDashboardTemplates + // Enable template dashboards + FlagDashboardTemplates = "dashboardTemplates" + + // FlagKubernetesAnnotations + // Enables app platform API for annotations + FlagKubernetesAnnotations = "kubernetesAnnotations" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2019ca4b976..4011467144c 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -6,13 +6,16 @@ { "metadata": { "name": "addFieldFromCalculationStatFunctions", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-03T14:39:58Z" + "resourceVersion": "1762442825881", + "creationTimestamp": "2023-11-03T14:39:58Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" + } }, "spec": { "description": "Add cumulative and window functions to the add field from calculation transformation", "stage": "GA", - "codeowner": "@grafana/dataviz-squad", + "codeowner": "@grafana/datapro", "frontend": true, "expression": "true" } @@ -22,6 +25,7 @@ "name": "adhocFiltersInTooltips", "resourceVersion": "1756814786992", "creationTimestamp": "2025-07-29T17:53:43Z", + "deletionTimestamp": "2025-11-11T09:49:48Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" } @@ -853,7 +857,8 @@ "metadata": { "name": "canvasPanelNesting", "resourceVersion": "1753448760331", - "creationTimestamp": "2022-05-31T19:03:34Z" + "creationTimestamp": "2022-05-31T19:03:34Z", + "deletionTimestamp": "2025-11-06T14:43:27Z" }, "spec": { "description": "Allow elements nesting", @@ -1001,13 +1006,16 @@ { "metadata": { "name": "correlations", - "resourceVersion": "1753448760331", - "creationTimestamp": "2022-09-16T13:14:27Z" + "resourceVersion": "1762442825881", + "creationTimestamp": "2022-09-16T13:14:27Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" + } }, "spec": { "description": "Correlations page", "stage": "GA", - "codeowner": "@grafana/dataviz-squad", + "codeowner": "@grafana/datapro", "allowSelfServe": true, "expression": "true" } @@ -1054,6 +1062,7 @@ "name": "dashboardDsAdHocFiltering", "resourceVersion": "1756814786992", "creationTimestamp": "2025-07-23T08:12:25Z", + "deletionTimestamp": "2025-09-27T19:59:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" } @@ -1082,14 +1091,14 @@ { "metadata": { "name": "dashboardLibrary", - "resourceVersion": "1760051989635", + "resourceVersion": "1762521182817", "creationTimestamp": "2025-09-26T16:02:12Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-10-09 23:19:49.635811 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-11-07 13:13:02.817210943 +0000 UTC" } }, "spec": { - "description": "Enable suggested dashboards when creating new dashboards", + "description": "Enable dashboard library experiments that are production ready", "stage": "experimental", "codeowner": "@grafana/sharing-squad" } @@ -1161,6 +1170,18 @@ "codeowner": "@grafana/grafana-app-platform-squad" } }, + { + "metadata": { + "name": "dashboardTemplates", + "resourceVersion": "1761575312733", + "creationTimestamp": "2025-10-27T14:28:32Z" + }, + "spec": { + "description": "Enable template dashboards", + "stage": "experimental", + "codeowner": "@grafana/sharing-squad" + } + }, { "metadata": { "name": "dashboardUndoRedo", @@ -1299,13 +1320,16 @@ { "metadata": { "name": "disableSSEDataplane", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-04-12T16:24:34Z" + "resourceVersion": "1762552416963", + "creationTimestamp": "2023-04-12T16:24:34Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-07 21:53:36.963146843 +0000 UTC" + } }, "spec": { "description": "Disables dataplane specific processing in server side expressions.", "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "codeowner": "@grafana/grafana-datasources-core-services" } }, { @@ -1331,8 +1355,11 @@ { "metadata": { "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-01-24T09:43:44Z" + "resourceVersion": "1762783224740", + "creationTimestamp": "2023-01-24T09:43:44Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-10 14:00:24.740459 +0000 UTC" + } }, "spec": { "description": "Enables drag and drop for CSV and Excel files", @@ -1594,13 +1621,17 @@ { "metadata": { "name": "extractFieldsNameDeduplication", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-02T15:47:42Z" + "resourceVersion": "1762442825881", + "creationTimestamp": "2023-11-02T15:47:42Z", + "deletionTimestamp": "2025-11-11T16:00:57Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" + } }, "spec": { "description": "Make sure extracted field names are unique in the dataframe", "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", + "codeowner": "@grafana/datapro", "frontend": true } }, @@ -1723,13 +1754,16 @@ { "metadata": { "name": "formatString", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-10-13T18:17:12Z" + "resourceVersion": "1762442825881", + "creationTimestamp": "2023-10-13T18:17:12Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" + } }, "spec": { "description": "Enable format string transformer", "stage": "GA", - "codeowner": "@grafana/dataviz-squad", + "codeowner": "@grafana/datapro", "frontend": true, "expression": "true" } @@ -1895,13 +1929,16 @@ { "metadata": { "name": "groupToNestedTableTransformation", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-02-07T14:28:26Z" + "resourceVersion": "1762442825881", + "creationTimestamp": "2024-02-07T14:28:26Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" + } }, "spec": { "description": "Enables the group to nested table transformation", "stage": "GA", - "codeowner": "@grafana/dataviz-squad", + "codeowner": "@grafana/datapro", "frontend": true, "expression": "true" } @@ -2050,6 +2087,18 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "jaegerEnableGrpcEndpoint", + "resourceVersion": "1760451551713", + "creationTimestamp": "2025-10-14T14:19:11Z" + }, + "spec": { + "description": "Enable querying trace data through Jaeger's gRPC endpoint (HTTP)", + "stage": "experimental", + "codeowner": "@grafana/oss-big-tent" + } + }, { "metadata": { "name": "jitterAlertRulesWithinGroups", @@ -2129,6 +2178,19 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "kubernetesAnnotations", + "resourceVersion": "1761142826172", + "creationTimestamp": "2025-10-22T14:20:26Z" + }, + "spec": { + "description": "Enables app platform API for annotations", + "stage": "experimental", + "codeowner": "@grafana/grafana-backend-services-squad", + "expression": "false" + } + }, { "metadata": { "name": "kubernetesAuthZHandlerRedirect", @@ -2751,14 +2813,18 @@ { "metadata": { "name": "newLogsPanel", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-02-04T17:40:17Z" + "resourceVersion": "1762166984808", + "creationTimestamp": "2025-02-04T17:40:17Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-03 10:49:44.808226 +0000 UTC" + } }, "spec": { - "description": "Enables the new logs panel in Explore", - "stage": "experimental", + "description": "Enables the new logs panel", + "stage": "GA", "codeowner": "@grafana/observability-logs", - "frontend": true + "frontend": true, + "expression": "true" } }, { @@ -2864,7 +2930,8 @@ "metadata": { "name": "panelMonitoring", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-10-09T05:19:08Z" + "creationTimestamp": "2023-10-09T05:19:08Z", + "deletionTimestamp": "2025-11-06T15:46:51Z" }, "spec": { "description": "Enables panel monitoring through logs and measurements", @@ -2874,6 +2941,18 @@ "expression": "true" } }, + { + "metadata": { + "name": "panelTimeSettings", + "resourceVersion": "1761555646368", + "creationTimestamp": "2025-10-27T09:00:46Z" + }, + "spec": { + "description": "Enables a new panel time settings drawer", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad" + } + }, { "metadata": { "name": "panelTitleSearch", @@ -3040,7 +3119,8 @@ "metadata": { "name": "pluginsFrontendSandbox", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-05T08:51:36Z" + "creationTimestamp": "2023-06-05T08:51:36Z", + "deletionTimestamp": "2025-11-05T10:06:22Z" }, "spec": { "description": "Enables the plugins frontend sandbox", @@ -3103,7 +3183,8 @@ "metadata": { "name": "preinstallAutoUpdate", "resourceVersion": "1753448760331", - "creationTimestamp": "2024-11-07T12:14:25Z" + "creationTimestamp": "2024-11-07T12:14:25Z", + "deletionTimestamp": "2025-11-07T10:10:01Z" }, "spec": { "description": "Enables automatic updates for pre-installed plugins", @@ -3354,7 +3435,8 @@ "metadata": { "name": "recordedQueriesMulti", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-14T12:34:22Z" + "creationTimestamp": "2023-06-14T12:34:22Z", + "deletionTimestamp": "2025-11-07T17:31:39Z" }, "spec": { "description": "Enables writing multiple items from a single query within Recorded Queries", @@ -3379,14 +3461,17 @@ { "metadata": { "name": "regressionTransformation", - "resourceVersion": "1753448760331", + "resourceVersion": "1762442825881", "creationTimestamp": "2023-11-24T14:49:16Z", - "deletionTimestamp": "2025-07-01T13:59:22Z" + "deletionTimestamp": "2025-07-01T13:59:22Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" + } }, "spec": { "description": "Enables regression analysis transformation", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "codeowner": "@grafana/datapro", "frontend": true } }, @@ -3587,15 +3672,18 @@ { "metadata": { "name": "sharingDashboardImage", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-15T21:07:39Z" + "resourceVersion": "1761782859498", + "creationTimestamp": "2025-07-15T21:07:39Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-10-30 00:07:39.498343 +0000 UTC" + } }, "spec": { "description": "Enables image sharing functionality for dashboards", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/sharing-squad", "frontend": true, - "hideFromDocs": true + "expression": "true" } }, { @@ -3672,13 +3760,16 @@ { "metadata": { "name": "sseGroupByDatasource", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-09-07T20:02:07Z" + "resourceVersion": "1762552416963", + "creationTimestamp": "2023-09-07T20:02:07Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-07 21:53:36.963146843 +0000 UTC" + } }, "spec": { "description": "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.", "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "codeowner": "@grafana/grafana-datasources-core-services" } }, { @@ -3722,6 +3813,18 @@ "codeowner": "@grafana/search-and-storage" } }, + { + "metadata": { + "name": "suggestedDashboards", + "resourceVersion": "1762521182817", + "creationTimestamp": "2025-11-07T13:13:02Z" + }, + "spec": { + "description": "Enable suggested dashboards when creating new dashboards", + "stage": "experimental", + "codeowner": "@grafana/sharing-squad" + } + }, { "metadata": { "name": "tableNextGen", @@ -3872,6 +3975,22 @@ "frontend": true } }, + { + "metadata": { + "name": "timeRangePan", + "resourceVersion": "1762290731154", + "creationTimestamp": "2025-10-24T19:49:53Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-04 21:12:11.154822 +0000 UTC" + } + }, + "spec": { + "description": "Enables time range panning functionality", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, { "metadata": { "name": "timeRangeProvider", diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index bad681c60a8..a78d1d76b7c 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -1241,15 +1241,16 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold s.log.ErrorContext(ctx, "failed to get descendant folders", "error", err) return descendantUIDs, err } + descendants = folder.SortByPostorder(descendants) for _, f := range descendants { descendantUIDs = append(descendantUIDs, f.UID) } - s.log.InfoContext(ctx, "deleting folder descendants", "org_id", cmd.OrgID, "uid", cmd.UID) + s.log.InfoContext(ctx, "deleting legacy folder descendants", "org_id", cmd.OrgID, "uid", cmd.UID, "descendantsUIDs", strings.Join(descendantUIDs, ",")) err = s.store.Delete(ctx, descendantUIDs, cmd.OrgID) if err != nil { - s.log.InfoContext(ctx, "failed deleting descendants", "org_id", cmd.OrgID, "parent_uid", cmd.UID, "err", err) + s.log.ErrorContext(ctx, "failed to delete legacy folder descendants", "org_id", cmd.OrgID, "parent_uid", cmd.UID, "descendantsUIDs", strings.Join(descendantUIDs, ","), "err", err) return descendantUIDs, err } return descendantUIDs, nil diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index c6a78fc0d2c..1551d858efe 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -618,12 +618,14 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol if err != nil { return err } + descFolders = folder.SortByPostorder(descFolders) folders := []string{} for _, f := range descFolders { folders = append(folders, f.UID) } // must delete children first, then the parent folder + s.log.InfoContext(ctx, "deleting folder with descendants", "org_id", cmd.OrgID, "uid", cmd.UID, "folderUIDs", strings.Join(folders, ",")) folders = append(folders, cmd.UID) if cmd.ForceDeleteRules { @@ -644,6 +646,21 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder) } + libraryPanelSrv, ok := s.registry[entity.StandardKindLibraryPanel] + if !ok { + return folder.ErrInternal.Errorf("no library panel service found in registry") + } + // /* TODO: after a decision regarding folder deletion permissions has been made + // (https://github.com/grafana/grafana-enterprise/issues/5144), + // remove the following call to DeleteInFolders + // and remove "user" from the signature of DeleteInFolder in the folder RegistryService. + // Context: https://github.com/grafana/grafana/pull/69149#discussion_r1235057903 + // */ + // Obs: DeleteInFolders only deletes dangling library panels (not linked to any dashboard) and throws errors if there are connections + if err := libraryPanelSrv.DeleteInFolders(ctx, cmd.OrgID, folders, cmd.SignedInUser); err != nil { + s.log.Error("failed to delete dangling library panels in folders", "error", err, "folders", strings.Join(folders, ",")) + return err + } // We need a list of dashboard uids inside the folder to delete related dashboards & public dashboards - // we cannot use the dashboard service directly due to circular dependencies, so use the search client to get the dashboards request := &resourcepb.ResourceSearchRequest{ diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index bf4d96c976e..0ff7a25b0e9 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -17,6 +17,7 @@ import ( clientrest "k8s.io/client-go/rest" folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/bus" @@ -33,6 +34,9 @@ import ( dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" "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/libraryelements" + "github.com/grafana/grafana/pkg/services/librarypanels" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/search/model" @@ -209,6 +213,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { {Action: dashboards.ActionFoldersDelete, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll}, {Action: accesscontrol.ActionAlertingRuleDelete, Scope: dashboards.ScopeFoldersAll}, + {Action: accesscontrol.ActionLibraryPanelsDelete, Scope: dashboards.ScopeFoldersAll}, }), }} @@ -219,6 +224,16 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, } + mockDashboardService := dashboards.NewFakeDashboardService(t) + mockFolderService := foldertest.NewFakeService() + elementService := libraryelements.ProvideService(cfg, db, routing.NewRouteRegister(), mockFolderService, featuremgmt.WithFeatures(), actest.FakeAccessControl{ExpectedEvaluate: true}, mockDashboardService, nil, nil) + lps := librarypanels.LibraryPanelService{ + Cfg: cfg, + SQLStore: db, + LibraryElementService: elementService, + FolderService: mockFolderService, + } + publicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) fakeK8sClient := new(client.MockK8sHandler) @@ -237,6 +252,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { } require.NoError(t, folderService.RegisterService(alertingStore)) + require.NoError(t, folderService.RegisterService(lps)) t.Run("Folder service tests", func(t *testing.T) { t.Run("Given user has no permissions", func(t *testing.T) { @@ -864,6 +880,7 @@ func TestIntegrationDeleteFoldersFromApiServer(t *testing.T) { registry: make(map[string]folder.RegistryService), features: featuremgmt.WithFeatures(), tracer: tracer, + log: slog.New(logtest.NewNopHandler(t)), } user := &user.SignedInUser{OrgID: 1} ctx := identity.WithRequester(context.Background(), user) @@ -877,6 +894,17 @@ func TestIntegrationDeleteFoldersFromApiServer(t *testing.T) { } require.NoError(t, service.RegisterService(alertingStore)) + mockDashboardService := dashboards.NewFakeDashboardService(t) + mockFolderService := foldertest.NewFakeService() + elementService := libraryelements.ProvideService(cfg, db, routing.NewRouteRegister(), mockFolderService, featuremgmt.WithFeatures(), actest.FakeAccessControl{ExpectedEvaluate: true}, mockDashboardService, nil, nil) + lps := librarypanels.LibraryPanelService{ + Cfg: cfg, + SQLStore: db, + LibraryElementService: elementService, + FolderService: mockFolderService, + } + require.NoError(t, service.RegisterService(lps)) + t.Run("Should delete folder", func(t *testing.T) { publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{}).Return(nil).Once() dashboardK8sclient.On("Search", mock.Anything, int64(1), mock.Anything).Return(&resourcepb.ResourceSearchResponse{Results: &resourcepb.ResourceTable{}}, nil).Once() diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index 28ced5f2321..4e2aa02343e 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -388,7 +388,9 @@ func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFo } if (q.WithFullpath || q.WithFullpathUIDs) && f.Fullpath == "" { - buildFolderFullPaths(f, relations, folderMap) + if err := buildFolderFullPaths(f, relations, folderMap); err != nil { + return nil, err + } } hits = append(hits, f) @@ -437,7 +439,10 @@ func (ss *FolderUnifiedStoreImpl) GetDescendants(ctx context.Context, orgID int6 } descendantsMap := map[string]*folder.Folder{} - getDescendants(nodes, tree, ancestor_uid, descendantsMap) + err = getDescendants(nodes, tree, ancestor_uid, descendantsMap, nil) + if err != nil { + return nil, err + } descendants := []*folder.Folder{} for _, f := range descendantsMap { @@ -447,11 +452,27 @@ func (ss *FolderUnifiedStoreImpl) GetDescendants(ctx context.Context, orgID int6 return descendants, nil } -func getDescendants(nodes map[string]*folder.Folder, tree map[string]map[string]*folder.Folder, ancestor_uid string, descendantsMap map[string]*folder.Folder) { - for uid := range tree[ancestor_uid] { - descendantsMap[uid] = nodes[uid] - getDescendants(nodes, tree, uid, descendantsMap) +func getDescendants( + nodes map[string]*folder.Folder, + tree map[string]map[string]*folder.Folder, + ancestorUID string, + descendantsMap map[string]*folder.Folder, + seen map[string]bool, +) error { + if seen == nil { + seen = map[string]bool{} } + if seen[ancestorUID] { + return folder.ErrCircularReference.Errorf("circular reference detected at folder uid: %s", ancestorUID) + } + seen[ancestorUID] = true + for uid := range tree[ancestorUID] { + descendantsMap[uid] = nodes[uid] + if err := getDescendants(nodes, tree, uid, descendantsMap, seen); err != nil { + return err + } + } + return nil } func (ss *FolderUnifiedStoreImpl) CountFolderContent(ctx context.Context, orgID int64, ancestor_uid string) (folder.DescendantCounts, error) { @@ -559,15 +580,20 @@ func computeFullPath(parents []*folder.Folder) (string, string) { return strings.Join(fullpath, "/"), strings.Join(fullpathUIDs, "/") } -func buildFolderFullPaths(f *folder.Folder, relations map[string]string, folderMap map[string]*folder.Folder) { +func buildFolderFullPaths(f *folder.Folder, relations map[string]string, folderMap map[string]*folder.Folder) error { titles := make([]string, 0) uids := make([]string, 0) titles = append(titles, f.Title) uids = append(uids, f.UID) + seen := make(map[string]bool) currentUID := f.UID for currentUID != "" { + if seen[currentUID] { + return folder.ErrCircularReference.Errorf("circular reference detected for folder %s", currentUID) + } + seen[currentUID] = true parentUID, exists := relations[currentUID] if !exists { break @@ -588,6 +614,7 @@ func buildFolderFullPaths(f *folder.Folder, relations map[string]string, folderM f.Fullpath = strings.Join(util.Reverse(titles), "/") f.FullpathUIDs = strings.Join(util.Reverse(uids), "/") + return nil } func shouldSkipFolder(f *folder.Folder, filterUIDs map[string]struct{}) bool { diff --git a/pkg/services/folder/folderimpl/unifiedstore_test.go b/pkg/services/folder/folderimpl/unifiedstore_test.go index a2bb1db024d..6dc8379a1a8 100644 --- a/pkg/services/folder/folderimpl/unifiedstore_test.go +++ b/pkg/services/folder/folderimpl/unifiedstore_test.go @@ -765,6 +765,279 @@ func TestGetFolders(t *testing.T) { } } +func TestGetDescendants(t *testing.T) { + orgID := int64(1) + + type args struct { + ctx context.Context + orgID int64 + ancestorUID string + } + tests := []struct { + name string + args args + mock func(mockCli *client.MockK8sHandler) + want []*folder.Folder + wantErr bool + }{ + { + name: "should return all descendants in a tree structure", + args: args{ + ctx: context.Background(), + orgID: orgID, + ancestorUID: "root", + }, + mock: func(mockCli *client.MockK8sHandler) { + mockCli.On("List", mock.Anything, orgID, metav1.ListOptions{ + Limit: folderListLimit, + TypeMeta: metav1.TypeMeta{}, + }).Return(&unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "root", + "uid": "root", + }, + "spec": map[string]interface{}{ + "title": "Root", + }, + }, + }, + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "child1", + "uid": "child1", + "annotations": map[string]interface{}{"grafana.app/folder": "root"}, + }, + "spec": map[string]interface{}{ + "title": "Child1", + }, + }, + }, + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "child2", + "uid": "child2", + "annotations": map[string]interface{}{"grafana.app/folder": "child1"}, + }, + "spec": map[string]interface{}{ + "title": "Child2", + }, + }, + }, + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "child3", + "uid": "child3", + "annotations": map[string]interface{}{"grafana.app/folder": "root"}, + }, + "spec": map[string]interface{}{ + "title": "Child3", + }, + }, + }, + }, + }, nil).Once() + }, + want: []*folder.Folder{ + { + UID: "child1", + Title: "Child1", + OrgID: orgID, + }, + { + UID: "child2", + Title: "Child2", + OrgID: orgID, + }, + { + UID: "child3", + Title: "Child3", + OrgID: orgID, + }, + }, + wantErr: false, + }, + { + name: "should return empty list when ancestor has no descendants", + args: args{ + ctx: context.Background(), + orgID: orgID, + ancestorUID: "leaf", + }, + mock: func(mockCli *client.MockK8sHandler) { + mockCli.On("List", mock.Anything, orgID, metav1.ListOptions{ + Limit: folderListLimit, + TypeMeta: metav1.TypeMeta{}, + }).Return(&unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "leaf", + "uid": "leaf", + }, + "spec": map[string]interface{}{ + "title": "Leaf", + }, + }, + }, + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "other", + "uid": "other", + "annotations": map[string]interface{}{"grafana.app/folder": "parent"}, + }, + "spec": map[string]interface{}{ + "title": "Other", + }, + }, + }, + }, + }, nil).Once() + }, + want: []*folder.Folder{}, + wantErr: false, + }, + { + name: "should detect circular reference and return error", + args: args{ + ctx: context.Background(), + orgID: orgID, + ancestorUID: "a", + }, + mock: func(mockCli *client.MockK8sHandler) { + mockCli.On("List", mock.Anything, orgID, metav1.ListOptions{ + Limit: folderListLimit, + TypeMeta: metav1.TypeMeta{}, + }).Return(&unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "a", + "uid": "a", + "annotations": map[string]interface{}{"grafana.app/folder": "c"}, + }, + "spec": map[string]interface{}{ + "title": "A", + }, + }, + }, + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "b", + "uid": "b", + "annotations": map[string]interface{}{"grafana.app/folder": "a"}, + }, + "spec": map[string]interface{}{ + "title": "B", + }, + }, + }, + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "c", + "uid": "c", + "annotations": map[string]interface{}{"grafana.app/folder": "b"}, + }, + "spec": map[string]interface{}{ + "title": "C", + }, + }, + }, + }, + }, nil).Once() + }, + want: nil, + wantErr: true, + }, + { + name: "should detect self-referencing cycle and return error", + args: args{ + ctx: context.Background(), + orgID: orgID, + ancestorUID: "self", + }, + mock: func(mockCli *client.MockK8sHandler) { + mockCli.On("List", mock.Anything, orgID, metav1.ListOptions{ + Limit: folderListLimit, + TypeMeta: metav1.TypeMeta{}, + }).Return(&unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "self", + "uid": "self", + "annotations": map[string]interface{}{"grafana.app/folder": "child"}, + }, + "spec": map[string]interface{}{ + "title": "Self", + }, + }, + }, + { + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "child", + "uid": "child", + "annotations": map[string]interface{}{"grafana.app/folder": "self"}, + }, + "spec": map[string]interface{}{ + "title": "Child", + }, + }, + }, + }, + }, nil).Once() + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCLI := new(client.MockK8sHandler) + tt.mock(mockCLI) + tracer := noop.NewTracerProvider().Tracer("TestGetDescendants") + ss := &FolderUnifiedStoreImpl{ + k8sclient: mockCLI, + userService: usertest.NewUserServiceFake(), + tracer: tracer, + } + got, err := ss.GetDescendants(tt.args.ctx, tt.args.orgID, tt.args.ancestorUID) + if tt.wantErr { + require.ErrorIs(t, err, folder.ErrCircularReference) + return + } + require.NoError(t, err) + require.Len(t, got, len(tt.want)) + + // Create a map for easier comparison since order may vary + gotMap := make(map[string]*folder.Folder) + for _, f := range got { + gotMap[f.UID] = f + } + require.Len(t, gotMap, len(tt.want)) + + for _, want := range tt.want { + gotFolder, exists := gotMap[want.UID] + require.True(t, exists, "Expected folder with UID %s not found", want.UID) + require.Equal(t, want.Title, gotFolder.Title) + require.Equal(t, want.OrgID, gotFolder.OrgID) + } + }) + } +} + func TestBuildFolderFullPaths(t *testing.T) { type args struct { f *folder.Folder @@ -881,7 +1154,7 @@ func TestBuildFolderFullPaths(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - buildFolderFullPaths(tt.args.f, tt.args.relations, tt.args.folderMap) + require.NoError(t, buildFolderFullPaths(tt.args.f, tt.args.relations, tt.args.folderMap)) require.Equal(t, tt.want.Fullpath, tt.args.f.Fullpath, "BuildFolderFullPaths() = %v, want %v", tt.args.f.Fullpath, tt.want.Fullpath) require.Equal(t, tt.want.FullpathUIDs, tt.args.f.FullpathUIDs, "BuildFolderFullPaths() = %v, want %v", tt.args.f.FullpathUIDs, tt.want.FullpathUIDs) require.Equal(t, tt.want.Title, tt.args.f.Title, "BuildFolderFullPaths() = %v, want %v", tt.args.f.Title, tt.want.Title) @@ -891,6 +1164,141 @@ func TestBuildFolderFullPaths(t *testing.T) { } } +func TestBuildFolderFullPaths_CircularReference(t *testing.T) { + type args struct { + f *folder.Folder + relations map[string]string + folderMap map[string]*folder.Folder + } + tests := []struct { + name string + args args + expectedErr string + }{ + { + name: "should detect direct circular reference (A -> B -> A)", + args: args{ + f: &folder.Folder{ + Title: "FolderA", + UID: "folder-a", + ParentUID: "folder-b", + }, + relations: map[string]string{ + "folder-a": "folder-b", + "folder-b": "folder-a", // circular: B points back to A + }, + folderMap: map[string]*folder.Folder{ + "folder-a": { + Title: "FolderA", + UID: "folder-a", + ParentUID: "folder-b", + }, + "folder-b": { + Title: "FolderB", + UID: "folder-b", + ParentUID: "folder-a", + }, + }, + }, + expectedErr: "circular reference detected", + }, + { + name: "should detect self-reference (A -> A)", + args: args{ + f: &folder.Folder{ + Title: "FolderA", + UID: "folder-a", + ParentUID: "folder-a", // points to itself + }, + relations: map[string]string{ + "folder-a": "folder-a", + }, + folderMap: map[string]*folder.Folder{ + "folder-a": { + Title: "FolderA", + UID: "folder-a", + ParentUID: "folder-a", + }, + }, + }, + expectedErr: "circular reference detected", + }, + { + name: "should detect longer circular reference (A -> B -> C -> A)", + args: args{ + f: &folder.Folder{ + Title: "FolderA", + UID: "folder-a", + ParentUID: "folder-b", + }, + relations: map[string]string{ + "folder-a": "folder-b", + "folder-b": "folder-c", + "folder-c": "folder-a", // circular: C points back to A + }, + folderMap: map[string]*folder.Folder{ + "folder-a": { + Title: "FolderA", + UID: "folder-a", + ParentUID: "folder-b", + }, + "folder-b": { + Title: "FolderB", + UID: "folder-b", + ParentUID: "folder-c", + }, + "folder-c": { + Title: "FolderC", + UID: "folder-c", + ParentUID: "folder-a", + }, + }, + }, + expectedErr: "circular reference detected", + }, + { + name: "should detect circular reference starting from middle (B in A -> B -> C -> A)", + args: args{ + f: &folder.Folder{ + Title: "FolderB", + UID: "folder-b", + ParentUID: "folder-c", + }, + relations: map[string]string{ + "folder-a": "folder-b", + "folder-b": "folder-c", + "folder-c": "folder-a", + }, + folderMap: map[string]*folder.Folder{ + "folder-a": { + Title: "FolderA", + UID: "folder-a", + ParentUID: "folder-b", + }, + "folder-b": { + Title: "FolderB", + UID: "folder-b", + ParentUID: "folder-c", + }, + "folder-c": { + Title: "FolderC", + UID: "folder-c", + ParentUID: "folder-a", + }, + }, + }, + expectedErr: "circular reference detected", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := buildFolderFullPaths(tt.args.f, tt.args.relations, tt.args.folderMap) + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedErr) + }) + } +} + func TestList(t *testing.T) { type args struct { ctx context.Context diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 5dbfa4e945c..3e59f5c1b6f 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -278,3 +278,50 @@ type GetDescendantCountsQuery struct { } type DescendantCounts map[string]int64 + +// SortByPostorder returns the folders in postorder traversal order. +// That is, children folders appear before their parents in the returned slice. +func SortByPostorder(folders []*Folder) []*Folder { + if len(folders) == 0 { + return folders + } + + // Build parent-to-children map + tree := make(map[string][]*Folder) + folderMap := make(map[string]*Folder) + for _, f := range folders { + folderMap[f.UID] = f + tree[f.ParentUID] = append(tree[f.ParentUID], f) + } + + // Find all roots (folders whose parents are not in the result set) + var roots []*Folder + for _, f := range folders { + if folderMap[f.ParentUID] == nil { + roots = append(roots, f) + } + } + + // Traverse in postorder + result := make([]*Folder, 0, len(folders)) + visited := make(map[string]bool) + var traverse func(f *Folder) + traverse = func(f *Folder) { + if visited[f.UID] { + return + } + visited[f.UID] = true + // First visit all children + for _, child := range tree[f.UID] { + traverse(child) + } + // Then add current folder + result = append(result, f) + } + + for _, root := range roots { + traverse(root) + } + + return result +} diff --git a/pkg/services/folder/model_test.go b/pkg/services/folder/model_test.go new file mode 100644 index 00000000000..0fb747d2a6b --- /dev/null +++ b/pkg/services/folder/model_test.go @@ -0,0 +1,153 @@ +package folder + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFoldersSortByPostorder(t *testing.T) { + t.Run("empty list returns empty list", func(t *testing.T) { + var folders []*Folder + result := SortByPostorder(folders) + require.Empty(t, result) + }) + + t.Run("single folder returns single folder", func(t *testing.T) { + folders := []*Folder{ + {UID: "a", ParentUID: "root"}, + } + result := SortByPostorder(folders) + require.Len(t, result, 1) + require.Equal(t, "a", result[0].UID) + }) + + t.Run("linear hierarchy orders children before parents", func(t *testing.T) { + // Structure: root -> a -> b -> c + folders := []*Folder{ + {UID: "a", ParentUID: "root"}, + {UID: "c", ParentUID: "b"}, + {UID: "b", ParentUID: "a"}, + } + result := SortByPostorder(folders) + require.Len(t, result, 3) + // Postorder: c, b, a (children before parents) + require.Equal(t, "c", result[0].UID) + require.Equal(t, "b", result[1].UID) + require.Equal(t, "a", result[2].UID) + }) + + t.Run("branching hierarchy orders all children before their parent", func(t *testing.T) { + // Structure: + // root + // | + // a + // / | \ + // b c d + folders := []*Folder{ + {UID: "a", ParentUID: "root"}, + {UID: "b", ParentUID: "a"}, + {UID: "c", ParentUID: "a"}, + {UID: "d", ParentUID: "a"}, + } + result := SortByPostorder(folders) + require.Len(t, result, 4) + // 'a' must come after all its children (b, c, d) + aIndex := -1 + for i, f := range result { + if f.UID == "a" { + aIndex = i + break + } + } + require.Equal(t, 3, aIndex, "parent 'a' should be last") + // All children should come before parent + for i := 0; i < 3; i++ { + require.Contains(t, []string{"b", "c", "d"}, result[i].UID) + } + }) + + t.Run("deep hierarchy orders by depth", func(t *testing.T) { + // Structure: + // root + // | + // a + // | + // b + // / \ + // c d + // | + // e + folders := []*Folder{ + {UID: "a", ParentUID: "root"}, + {UID: "b", ParentUID: "a"}, + {UID: "c", ParentUID: "b"}, + {UID: "d", ParentUID: "b"}, + {UID: "e", ParentUID: "c"}, + } + result := SortByPostorder(folders) + require.Len(t, result, 5) + // e should come before c + eIndex := -1 + cIndex := -1 + for i, f := range result { + if f.UID == "e" { + eIndex = i + } + if f.UID == "c" { + cIndex = i + } + } + require.Less(t, eIndex, cIndex, "e should come before c") + // c and d should come before b + bIndex := -1 + dIndex := -1 + for i, f := range result { + if f.UID == "b" { + bIndex = i + } + if f.UID == "d" { + dIndex = i + } + } + require.Less(t, cIndex, bIndex, "c should come before b") + require.Less(t, dIndex, bIndex, "d should come before b") + // b should come before a + aIndex := -1 + for i, f := range result { + if f.UID == "a" { + aIndex = i + } + } + require.Less(t, bIndex, aIndex, "b should come before a") + }) + + t.Run("multiple subtrees maintains postorder per subtree", func(t *testing.T) { + // Structure: + // root1 root2 + // / \ | + // a b c + // | | + // d e + folders := []*Folder{ + {UID: "a", ParentUID: "root1"}, + {UID: "b", ParentUID: "root1"}, + {UID: "d", ParentUID: "b"}, + {UID: "c", ParentUID: "root2"}, + {UID: "e", ParentUID: "c"}, + } + result := SortByPostorder(folders) + require.Len(t, result, 5) + + // Find indices + indices := make(map[string]int) + for i, f := range result { + indices[f.UID] = i + } + + // Check postorder for first subtree: d before b, both before root1 + require.Less(t, indices["d"], indices["b"], "d should come before b") + // Check postorder for second subtree: e before c, both before root2 + require.Less(t, indices["e"], indices["c"], "e should come before c") + }) +} diff --git a/pkg/services/folder/store.go b/pkg/services/folder/store.go index 4ef49d912f4..949d8c4f0ac 100644 --- a/pkg/services/folder/store.go +++ b/pkg/services/folder/store.go @@ -46,7 +46,7 @@ type Store interface { // GetFolders returns folders with given uids GetFolders(ctx context.Context, q GetFoldersFromStoreQuery) ([]*Folder, error) - // GetDescendants returns all descendants of a folder + // GetDescendants returns all descendants of a folder (with no guaranteed order) GetDescendants(ctx context.Context, orgID int64, anchestor_uid string) ([]*Folder, error) // CountInOrg returns the number of folders in the given org diff --git a/pkg/services/frontend/frontend_settings.go b/pkg/services/frontend/frontend_settings.go index 9ffdb3dbad0..f6cf9855b2d 100644 --- a/pkg/services/frontend/frontend_settings.go +++ b/pkg/services/frontend/frontend_settings.go @@ -38,8 +38,9 @@ type FSFrontendSettings struct { AnalyticsConsoleReporting bool `json:"analyticsConsoleReporting,omitempty"` GrafanaJavascriptAgent setting.GrafanaJavascriptAgent `json:"grafanaJavascriptAgent,omitempty"` - ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString,omitempty"` - ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl,omitempty"` + ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString,omitempty"` + ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl,omitempty"` + ApplicationInsightsAutoRouteTracking bool `json:"applicationInsightsAutoRouteTracking,omitempty"` TrustedTypesDefaultPolicyEnabled bool `json:"trustedTypesDefaultPolicyEnabled,omitempty"` CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled,omitempty"` diff --git a/pkg/services/frontend/index.go b/pkg/services/frontend/index.go index 89f7fcaf066..22d04234a36 100644 --- a/pkg/services/frontend/index.go +++ b/pkg/services/frontend/index.go @@ -67,34 +67,35 @@ func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, li // subset of frontend settings needed for the login page // TODO what about enterprise settings here? frontendSettings := FSFrontendSettings{ - AnalyticsConsoleReporting: cfg.FrontendAnalyticsConsoleReporting, - AnonymousEnabled: cfg.Anonymous.Enabled, - ApplicationInsightsConnectionString: cfg.ApplicationInsightsConnectionString, - ApplicationInsightsEndpointUrl: cfg.ApplicationInsightsEndpointUrl, - AuthProxyEnabled: cfg.AuthProxy.Enabled, - AutoAssignOrg: cfg.AutoAssignOrg, - CSPReportOnlyEnabled: cfg.CSPReportOnlyEnabled, - DisableLoginForm: cfg.DisableLoginForm, - DisableUserSignUp: !cfg.AllowUserSignUp, - GoogleAnalytics4Id: cfg.GoogleAnalytics4ID, - GoogleAnalytics4SendManualPageViews: cfg.GoogleAnalytics4SendManualPageViews, - GoogleAnalyticsId: cfg.GoogleAnalyticsID, - GrafanaJavascriptAgent: cfg.GrafanaJavascriptAgent, - Http2Enabled: cfg.Protocol == setting.HTTP2Scheme, - JwtHeaderName: cfg.JWTAuth.HeaderName, - JwtUrlLogin: cfg.JWTAuth.URLLogin, - LdapEnabled: cfg.LDAPAuthEnabled, - LoginHint: cfg.LoginHint, - PasswordHint: cfg.PasswordHint, - ReportingStaticContext: cfg.ReportingStaticContext, - RudderstackConfigUrl: cfg.RudderstackConfigURL, - RudderstackDataPlaneUrl: cfg.RudderstackDataPlaneURL, - RudderstackIntegrationsUrl: cfg.RudderstackIntegrationsURL, - RudderstackSdkUrl: cfg.RudderstackSDKURL, - RudderstackWriteKey: cfg.RudderstackWriteKey, - TrustedTypesDefaultPolicyEnabled: (cfg.CSPEnabled && strings.Contains(cfg.CSPTemplate, "require-trusted-types-for")) || (cfg.CSPReportOnlyEnabled && strings.Contains(cfg.CSPReportOnlyTemplate, "require-trusted-types-for")), - VerifyEmailEnabled: cfg.VerifyEmailEnabled, - BuildInfo: getBuildInfo(license, cfg), + AnalyticsConsoleReporting: cfg.FrontendAnalyticsConsoleReporting, + AnonymousEnabled: cfg.Anonymous.Enabled, + ApplicationInsightsConnectionString: cfg.ApplicationInsightsConnectionString, + ApplicationInsightsEndpointUrl: cfg.ApplicationInsightsEndpointUrl, + ApplicationInsightsAutoRouteTracking: cfg.ApplicationInsightsAutoRouteTracking, + AuthProxyEnabled: cfg.AuthProxy.Enabled, + AutoAssignOrg: cfg.AutoAssignOrg, + CSPReportOnlyEnabled: cfg.CSPReportOnlyEnabled, + DisableLoginForm: cfg.DisableLoginForm, + DisableUserSignUp: !cfg.AllowUserSignUp, + GoogleAnalytics4Id: cfg.GoogleAnalytics4ID, + GoogleAnalytics4SendManualPageViews: cfg.GoogleAnalytics4SendManualPageViews, + GoogleAnalyticsId: cfg.GoogleAnalyticsID, + GrafanaJavascriptAgent: cfg.GrafanaJavascriptAgent, + Http2Enabled: cfg.Protocol == setting.HTTP2Scheme, + JwtHeaderName: cfg.JWTAuth.HeaderName, + JwtUrlLogin: cfg.JWTAuth.URLLogin, + LdapEnabled: cfg.LDAPAuthEnabled, + LoginHint: cfg.LoginHint, + PasswordHint: cfg.PasswordHint, + ReportingStaticContext: cfg.ReportingStaticContext, + RudderstackConfigUrl: cfg.RudderstackConfigURL, + RudderstackDataPlaneUrl: cfg.RudderstackDataPlaneURL, + RudderstackIntegrationsUrl: cfg.RudderstackIntegrationsURL, + RudderstackSdkUrl: cfg.RudderstackSDKURL, + RudderstackWriteKey: cfg.RudderstackWriteKey, + TrustedTypesDefaultPolicyEnabled: (cfg.CSPEnabled && strings.Contains(cfg.CSPTemplate, "require-trusted-types-for")) || (cfg.CSPReportOnlyEnabled && strings.Contains(cfg.CSPReportOnlyTemplate, "require-trusted-types-for")), + VerifyEmailEnabled: cfg.VerifyEmailEnabled, + BuildInfo: getBuildInfo(license, cfg), } return &IndexProvider{ @@ -153,6 +154,7 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http. p.runIndexDataHooks(reqCtx, &data) writer.Header().Set("Content-Type", "text/html; charset=UTF-8") + writer.Header().Set("Cache-Control", "no-store") writer.WriteHeader(200) if err := p.index.Execute(writer, &data); err != nil { if errors.Is(err, syscall.EPIPE) { // Client has stopped listening. diff --git a/pkg/services/frontend/index_test.go b/pkg/services/frontend/index_test.go index 3f0b76ac4f4..46fdeba4410 100644 --- a/pkg/services/frontend/index_test.go +++ b/pkg/services/frontend/index_test.go @@ -92,6 +92,7 @@ func TestFrontendService_WebAssets(t *testing.T) { assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Header().Get("Content-Type"), "text/html") + assert.Contains(t, recorder.Header().Get("Cache-Control"), "no-store") // The response should contain references to the assets body := recorder.Body.String() diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 791cef618a9..312905cf0c8 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -87,7 +87,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon } } - element, err := l.createLibraryElement(c.Req.Context(), c.SignedInUser, cmd) + element, err := l.CreateElement(c.Req.Context(), c.SignedInUser, cmd) if err != nil { return l.toLibraryElementError(err, "Failed to create library element") } @@ -124,7 +124,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon // 404: notFoundError // 500: internalServerError func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) response.Response { - id, err := l.deleteLibraryElement(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) + id, err := l.DeleteLibraryElement(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { return l.toLibraryElementError(err, "Failed to delete library element") } @@ -252,7 +252,7 @@ func (l *LibraryElementService) patchHandler(c *contextmodel.ReqContext) respons } } - element, err := l.patchLibraryElement(c.Req.Context(), c.SignedInUser, cmd, web.Params(c.Req)[":uid"]) + element, err := l.PatchLibraryElement(c.Req.Context(), c.SignedInUser, cmd, web.Params(c.Req)[":uid"]) if err != nil { return l.toLibraryElementError(err, "Failed to update library element") } diff --git a/pkg/services/libraryelements/conversions.go b/pkg/services/libraryelements/conversions.go new file mode 100644 index 00000000000..465830d1697 --- /dev/null +++ b/pkg/services/libraryelements/conversions.go @@ -0,0 +1,59 @@ +package libraryelements + +import ( + "encoding/json" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/libraryelements/model" + "github.com/grafana/grafana/pkg/util" +) + +func ToCreateLibraryElementCommand(raw runtime.Object) (*model.CreateLibraryElementCommand, error) { + obj, err := utils.MetaAccessor(raw) + if err != nil { + return nil, err + } + folder := obj.GetFolder() + cmd := &model.CreateLibraryElementCommand{ + UID: obj.GetName(), + FolderUID: &folder, + Kind: 1, // the only kind... LibraryPanel + Name: obj.FindTitle("library panel"), + } + if cmd.UID == "" { + if obj.GetGenerateName() == "" { + return nil, fmt.Errorf("expecting either name or generateName property") + } + cmd.UID = obj.GetGenerateName() + util.GenerateShortUID() + } + cmd.Model, err = toRawMessage(raw) + return cmd, err +} + +func ToPatchLibraryElementCommand(raw runtime.Object) (*model.PatchLibraryElementCommand, error) { + obj, err := utils.MetaAccessor(raw) + if err != nil { + return nil, err + } + folder := obj.GetFolder() + cmd := &model.PatchLibraryElementCommand{ + UID: obj.GetName(), + FolderUID: &folder, + Kind: 1, // the only kind... LibraryPanel + Name: obj.FindTitle("library panel"), + } + cmd.Model, err = toRawMessage(raw) + return cmd, err +} + +func toRawMessage(raw runtime.Object) (json.RawMessage, error) { + switch obj := raw.(type) { + case *v0alpha1.LibraryPanel: + return json.Marshal(obj.Spec) + } + return nil, fmt.Errorf("unsupported library panel type: %T", raw) +} diff --git a/pkg/services/libraryelements/conversions_test.go b/pkg/services/libraryelements/conversions_test.go new file mode 100644 index 00000000000..2ae76fb9fa4 --- /dev/null +++ b/pkg/services/libraryelements/conversions_test.go @@ -0,0 +1,101 @@ +package libraryelements + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/libraryelements/model" +) + +func TestConversionsCommands(t *testing.T) { + cases := []struct { + name string + input runtime.Object + expectedCreate *model.CreateLibraryElementCommand + expectedPatch *model.PatchLibraryElementCommand + }{ + { + name: "basic conversion", + input: &v0alpha1.LibraryPanel{ + ObjectMeta: metav1.ObjectMeta{ + Name: "uid", + Annotations: map[string]string{ + utils.AnnoKeyFolder: "aaa", + }, + }, + Spec: v0alpha1.LibraryPanelSpec{ + Type: "timeseries", + PluginVersion: "1.2.3", + Title: "title", + Description: "descr", + Options: common.Unstructured{ + Object: map[string]any{ + "hello": "options", + }, + }, + FieldConfig: common.Unstructured{ + Object: map[string]any{ + "hello": "fieldConfig", + }, + }, + PanelTitle: "panel title", + GridPos: v0alpha1.GridPos{ + W: 1, H: 2, X: 3, Y: 4, + }, + Transparent: true, + Links: []common.Unstructured{{ + Object: map[string]any{ + "link1": "hello", + }, + }}, + Datasource: &data.DataSourceRef{ + UID: "uid", + Type: "ttt", + APIVersion: "v0alpha1", + }, + }, + }, + expectedCreate: &model.CreateLibraryElementCommand{ + FolderUID: ptr.To("aaa"), + UID: "uid", + Name: "title", + Kind: 1, + Model: json.RawMessage(`{"type":"timeseries","pluginVersion":"1.2.3","title":"title","panelTitle":"panel title","description":"descr","options":{"hello":"options"},"fieldConfig":{"hello":"fieldConfig"},"datasource":{"type":"ttt","uid":"uid","apiVersion":"v0alpha1"},"gridPos":{"w":1,"h":2,"x":3,"y":4},"transparent":true,"links":[{"link1":"hello"}]}`), + }, + expectedPatch: &model.PatchLibraryElementCommand{ + FolderUID: ptr.To("aaa"), + UID: "uid", + Name: "title", + Kind: 1, + Version: 0, + Model: json.RawMessage(`{"type":"timeseries","pluginVersion":"1.2.3","title":"title","panelTitle":"panel title","description":"descr","options":{"hello":"options"},"fieldConfig":{"hello":"fieldConfig"},"datasource":{"type":"ttt","uid":"uid","apiVersion":"v0alpha1"},"gridPos":{"w":1,"h":2,"x":3,"y":4},"transparent":true,"links":[{"link1":"hello"}]}`), + }, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + create, err := ToCreateLibraryElementCommand(tt.input) + require.NoError(t, err) + if diff := cmp.Diff(tt.expectedCreate, create); diff != "" { + require.FailNowf(t, "Create mismatch (-want +got):%s", diff) + } + + patch, err := ToPatchLibraryElementCommand(tt.input) + require.NoError(t, err) + if diff := cmp.Diff(tt.expectedPatch, patch); diff != "" { + require.FailNowf(t, "Path mismatch (-want +got):%s", diff) + } + }) + } +} diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index d4923ffa20a..a5b43fd54cb 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -110,7 +110,7 @@ func (l *LibraryElementService) GetLibraryElement(c context.Context, signedInUse } // createLibraryElement adds a library element. -func (l *LibraryElementService) createLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { +func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { if err := l.requireSupportedElementKind(cmd.Kind); err != nil { return model.LibraryElementDTO{}, err } @@ -169,7 +169,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { allowed, err := l.AccessControl.Evaluate(c, signedInUser, ac.EvalPermission(ActionLibraryPanelsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID))) if !allowed { - return fmt.Errorf("insufficient permissions for creating library panel in folder with UID %s", folderUID) + return fmt.Errorf("insufficient permissions for creating library panel in folder with UID: '%s'", folderUID) } if err != nil { return err @@ -216,7 +216,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn } // deleteLibraryElement deletes a library element. -func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) { +func (l *LibraryElementService) DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) { var elementID int64 err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { element, err := l.GetLibraryElement(c, signedInUser, session, uid) @@ -473,17 +473,22 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI if err != nil { return err } + // Every signed in user can see the general folder. The general folder might have "general" or the empty string as its UID. - var folderUIDS = []string{"general", ""} - folderMap := map[string]string{} + // Using a map for O(1) lookup instead of O(n) slice iteration + folderUIDSet := make(map[string]bool, len(fs)+2) + folderUIDSet["general"] = true + folderUIDSet[""] = true + + folderMap := make(map[string]string, len(fs)) for _, f := range fs { - folderUIDS = append(folderUIDS, f.UID) + folderUIDSet[f.UID] = true folderMap[f.UID] = f.Title } // if the user is not an admin, we need to filter out elements that are not in folders the user can see for _, element := range elements { if !signedInUser.HasRole(org.RoleAdmin) { - if !contains(folderUIDS, element.FolderUID) { + if !folderUIDSet[element.FolderUID] { continue } } @@ -522,10 +527,11 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI }) } - var libraryElements []model.LibraryElement + var libraryElements []model.LibraryElementWithMeta countBuilder := db.SQLBuilder{} if folderFilter.includeGeneralFolder { countBuilder.Write(selectLibraryElementDTOWithMeta) + countBuilder.Write(", '' as folder_uid ") countBuilder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect())) countBuilder.Write(` WHERE le.org_id=? AND le.folder_id=0`, signedInUser.GetOrgID()) writeKindSQL(query, &countBuilder) @@ -537,6 +543,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI countBuilder.Write(" ") } countBuilder.Write(selectLibraryElementDTOWithMeta) + countBuilder.Write(", le.folder_uid as folder_uid ") countBuilder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect())) countBuilder.Write(` WHERE le.org_id=? AND le.folder_id<>0`, signedInUser.GetOrgID()) writeKindSQL(query, &countBuilder) @@ -550,8 +557,19 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI return err } + // Apply the same folder permission filtering to the count for non-admin users + totalCount := int64(len(libraryElements)) + if !signedInUser.HasRole(org.RoleAdmin) { + totalCount = 0 + for _, element := range libraryElements { + if folderUIDSet[element.FolderUID] { + totalCount++ + } + } + } + result = model.LibraryElementSearchResult{ - TotalCount: int64(len(libraryElements)), + TotalCount: totalCount, Elements: retDTOs, Page: query.Page, PerPage: query.PerPage, @@ -578,7 +596,7 @@ func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, eleme } // patchLibraryElement updates a Library Element. -func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) { +func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) { var dto model.LibraryElementDTO if err := l.requireSupportedElementKind(cmd.Kind); err != nil { return model.LibraryElementDTO{}, err @@ -878,15 +896,6 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c context.Conte }) } -func contains(slice []string, element string) bool { - for _, item := range slice { - if item == element { - return true - } - } - return false -} - func getFoldersWithMatchingTitles(c context.Context, l *LibraryElementService, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) ([]string, error) { if len(strings.TrimSpace(query.SearchString)) <= 0 { return nil, nil diff --git a/pkg/services/libraryelements/fake/libraryelements_service.go b/pkg/services/libraryelements/fake/libraryelements_service.go index e8f0b3615d5..13f7aef322d 100644 --- a/pkg/services/libraryelements/fake/libraryelements_service.go +++ b/pkg/services/libraryelements/fake/libraryelements_service.go @@ -17,6 +17,16 @@ type LibraryElementService struct { idCounter int64 } +// DeleteLibraryElement implements libraryelements.Service. +func (l *LibraryElementService) DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) { + panic("unimplemented") +} + +// PatchLibraryElement implements libraryelements.Service. +func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) { + panic("unimplemented") +} + var _ libraryelements.Service = (*LibraryElementService)(nil) func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { @@ -68,6 +78,11 @@ func (l *LibraryElementService) CreateElement(c context.Context, signedInUser id return dto, nil } +// PatchElement implements libraryelements.Service. +func (l *LibraryElementService) PatchElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) { + panic("unimplemented") +} + func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { l.mx.RLock() defer l.mx.RUnlock() diff --git a/pkg/services/libraryelements/k8s_conversion_test.go b/pkg/services/libraryelements/k8s_conversion_test.go index 0319e0ae76a..689ef1f0c1d 100644 --- a/pkg/services/libraryelements/k8s_conversion_test.go +++ b/pkg/services/libraryelements/k8s_conversion_test.go @@ -2,13 +2,13 @@ package libraryelements import ( "context" - "encoding/json" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/grafana/grafana/pkg/api/dtos" @@ -23,7 +23,6 @@ import ( "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestUnstructuredToLegacyLibraryPanelDTO(t *testing.T) { @@ -155,26 +154,22 @@ func TestUnstructuredToLegacyLibraryPanelDTO(t *testing.T) { require.Equal(t, testUser.Login, result.Meta.UpdatedBy.Name) require.Equal(t, dtos.GetGravatarUrl(cfg, testUser.Email), result.Meta.UpdatedBy.AvatarUrl) - var modelMap map[string]interface{} - err = json.Unmarshal(result.Model, &modelMap) - require.NoError(t, err) - require.Equal(t, "testdata", modelMap["datasource"].(map[string]interface{})["type"]) - require.Equal(t, "test-datasource", modelMap["datasource"].(map[string]interface{})["uid"]) - require.Equal(t, "Test description", modelMap["description"]) - require.Equal(t, float64(123), modelMap["id"]) - require.Equal(t, "text", modelMap["type"]) - require.Equal(t, "Test Panel Title", modelMap["title"]) - require.Equal(t, "Test content", modelMap["options"].(map[string]interface{})["content"]) - require.Equal(t, true, modelMap["transparent"]) - require.Equal(t, "Test Library Panel", modelMap["libraryPanel"].(map[string]interface{})["name"]) - require.Equal(t, "test-panel-uid", modelMap["libraryPanel"].(map[string]interface{})["uid"]) - links := modelMap["links"].([]interface{}) - require.Len(t, links, 1) - require.Equal(t, "Test Link", links[0].(map[string]interface{})["title"]) - - targets := modelMap["targets"].([]interface{}) - require.Len(t, targets, 1) - require.Equal(t, "A", targets[0].(map[string]interface{})["refId"]) + // fmt.Printf("%s\n", result.Model) + require.JSONEq(t, `{ + "datasource": { "type": "testdata", "uid": "test-datasource" }, + "description": "Test description", + "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" } } }, + "gridPos": { "w": 12, "h": 8, "x": 0, "y": 0 }, + "id": 123, + "libraryPanel": { "name": "Test Library Panel", "uid": "test-panel-uid" }, + "links": [{ "title": "Test Link", "url": "https://example.com" }], + "options": { "content": "Test content" }, + "pluginVersion": "1.0.0", + "targets": [{ "refId": "A", "expr": "test_query" }], + "title": "Test Panel Title", + "transparent": true, + "type": "text" + }`, string(result.Model)) dashboardsSvc.AssertExpectations(t) } diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index fe31dfcfc70..540e54fc817 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -40,6 +40,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout // Service is a service for operating on library elements. type Service interface { CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) + PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) + DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error @@ -63,11 +65,6 @@ type LibraryElementService struct { var _ Service = (*LibraryElementService)(nil) -// CreateElement creates a Library Element. -func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { - return l.createLibraryElement(c, signedInUser, cmd) -} - // GetElement gets an element from a UID. func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { return l.getLibraryElementByUid(c, signedInUser, cmd) diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index 36f2100308c..a48082a8d5e 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -14,13 +14,13 @@ import ( func TestIntegration_CreateLibraryElement(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) - scenarioWithPanel(t, "When an admin tries to create a library panel that already exists, it should fail", + scenarioWithPanel(t, "When an admin tries to create a library panel with the same name, it should succeed", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) - require.Equal(t, 400, resp.Status()) + require.Equal(t, 200, resp.Status()) }) scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists, it should succeed", diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index 612f5a9c279..7ff376cdecf 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -1370,4 +1370,161 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { require.NotEmpty(t, element.UID, "Should have a UID") require.Equal(t, int64(0), element.Meta.ConnectedDashboards, "Should have no connected dashboards") }) + + // Non-admin user permission tests + scenarioWithPanel(t, "When a non-admin user has folders but none of the library elements are in those folders, it should return empty result", + func(t *testing.T, sc scenarioContext) { + // Create library panels in the scenario folder + // nolint:staticcheck + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Create a different folder that the non-admin user has access to (but has no panels) + differentFolder := &folder.Folder{ + ID: 2, + OrgID: 1, + UID: "uid_for_DifferentFolder", + Title: "DifferentFolder", + } + + // Change user to non-admin and set their accessible folders to only the different folder + // This simulates a user who can see a folder but that folder doesn't contain any of the library elements + sc.reqContext.OrgRole = org.RoleViewer + sc.folderSvc.ExpectedFolders = []*folder.Folder{differentFolder} + sc.folderSvc.AddFolder(differentFolder) + + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err := json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + // TotalCount should be 0 for non-admin users since they can't access the folders with panels + require.Equal(t, int64(0), result.Result.TotalCount, "TotalCount should be 0 since user has no access to folders with panels") + require.Equal(t, 0, len(result.Result.Elements), "Elements should be empty since user has no access to folders with panels") + require.Equal(t, 1, result.Result.Page, "Should be on page 1") + require.Equal(t, 100, result.Result.PerPage, "Should have perPage 100") + }) + + scenarioWithPanel(t, "When a non-admin user has folders and some library elements are in those folders, it should return only accessible elements", + func(t *testing.T, sc scenarioContext) { + // Create a second folder that the non-admin user will have access to + accessibleFolder := &folder.Folder{ + ID: 2, + OrgID: 1, + UID: "uid_for_AccessibleFolder", + Title: "AccessibleFolder", + } + + // Create a library panel in the accessible folder (need to add it to fake service first) + sc.folderSvc.ExpectedFolder = accessibleFolder + sc.folderSvc.AddFolder(accessibleFolder) + // nolint:staticcheck + command := getCreatePanelCommand(accessibleFolder.ID, accessibleFolder.UID, "Accessible Panel") + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Create another panel in a folder the user won't have access to + inaccessibleFolder := &folder.Folder{ + ID: 3, + OrgID: 1, + UID: "uid_for_InaccessibleFolder", + Title: "InaccessibleFolder", + } + sc.folderSvc.ExpectedFolder = inaccessibleFolder + sc.folderSvc.AddFolder(inaccessibleFolder) + // nolint:staticcheck + command = getCreatePanelCommand(inaccessibleFolder.ID, inaccessibleFolder.UID, "Inaccessible Panel") + sc.reqContext.Req.Body = mockRequestBody(command) + resp = sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Change user to non-admin and set their accessible folders to only the accessible folder and scenario folder + // This will filter out the inaccessible folder + sc.reqContext.OrgRole = org.RoleViewer + sc.folderSvc.ExpectedFolders = []*folder.Folder{sc.folder, accessibleFolder} + + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err := json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + // TotalCount should match the number of accessible elements (2) for non-admin users + require.Equal(t, int64(2), result.Result.TotalCount, "TotalCount should be 2 (only accessible panels)") + require.Equal(t, 2, len(result.Result.Elements), "Elements should contain only 2 accessible panels") + require.Equal(t, 1, result.Result.Page, "Should be on page 1") + require.Equal(t, 100, result.Result.PerPage, "Should have perPage 100") + + // Verify the returned panels are from accessible folders only + folderUIDs := make(map[string]bool) + for _, element := range result.Result.Elements { + folderUIDs[element.FolderUID] = true + require.Contains(t, []string{sc.folder.UID, accessibleFolder.UID}, element.FolderUID, "Element should be in accessible folder") + require.NotEqual(t, inaccessibleFolder.UID, element.FolderUID, "Element should not be from inaccessible folder") + } + require.True(t, folderUIDs[sc.folder.UID], "Should include panel from scenario folder") + require.True(t, folderUIDs[accessibleFolder.UID], "Should include panel from accessible folder") + }) + + scenarioWithPanel(t, "When a non-admin user has access to all folders containing library elements, it should return all elements", + func(t *testing.T, sc scenarioContext) { + // Create a second folder that the non-admin user will have access to + folder2 := &folder.Folder{ + ID: 2, + OrgID: 1, + UID: "uid_for_Folder2", + Title: "Folder2", + } + sc.folderSvc.ExpectedFolder = folder2 + sc.folderSvc.AddFolder(folder2) + + // Create a library panel in folder2 + // nolint:staticcheck + command := getCreatePanelCommand(folder2.ID, folder2.UID, "Panel in Folder2") + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Create another panel in the original scenario folder + sc.folderSvc.ExpectedFolder = sc.folder + // nolint:staticcheck + command = getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Panel in ScenarioFolder") + sc.reqContext.Req.Body = mockRequestBody(command) + resp = sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Change user to non-admin and set their accessible folders to include all folders with panels + sc.reqContext.OrgRole = org.RoleViewer + sc.folderSvc.ExpectedFolders = []*folder.Folder{sc.folder, folder2} + + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err := json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + // Should return all 3 panels (1 from initial setup + 2 created in this test) + require.Equal(t, int64(3), result.Result.TotalCount, "Should return all 3 panels") + require.Equal(t, 3, len(result.Result.Elements), "Should have 3 elements") + require.Equal(t, 1, result.Result.Page, "Should be on page 1") + require.Equal(t, 100, result.Result.PerPage, "Should have perPage 100") + + // Verify all panels are from the accessible folders + folderUIDs := make(map[string]int) + for _, element := range result.Result.Elements { + folderUIDs[element.FolderUID]++ + require.Contains(t, []string{sc.folder.UID, folder2.UID}, element.FolderUID, "All elements should be in accessible folders") + require.Equal(t, int64(model.PanelElement), element.Kind, "Should be a panel element") + require.Equal(t, "text", element.Type, "Should be text panel") + } + require.Equal(t, 2, folderUIDs[sc.folder.UID], "Should have 2 panels in scenario folder") + require.Equal(t, 1, folderUIDs[folder2.UID], "Should have 1 panel in folder2") + }) } diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index c85c22ee676..1d9912195dd 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -103,7 +103,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) { newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll} sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll} - result, err := sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ + result, err := sc.service.CreateElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, Name: "Testing Library Panel With Deleted Folder", diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index 0cf29a83857..68f61302f41 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -332,7 +332,7 @@ func TestIntegration_PatchLibraryElement(t *testing.T) { } }) - scenarioWithPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should fail", + scenarioWithPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should pass", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Another Panel") @@ -348,10 +348,10 @@ func TestIntegration_PatchLibraryElement(t *testing.T) { sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, 400, resp.Status()) + require.Equal(t, 200, resp.Status()) }) - scenarioWithPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail", + scenarioWithPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should pass", func(t *testing.T, sc scenarioContext) { newFolder := &folder.Folder{ ID: 2, @@ -375,7 +375,7 @@ func TestIntegration_PatchLibraryElement(t *testing.T) { sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) resp := sc.service.patchHandler(sc.reqContext) - require.Equal(t, 400, resp.Status()) + require.Equal(t, 200, resp.Status()) }) scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail", diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index e782c20ac03..9340c19409f 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -150,7 +150,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) { newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll} sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll} - _, err = sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ + _, err = sc.service.CreateElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, Name: "Testing Library Panel With Deleted Folder", diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index 095e3390ce9..e8922772240 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -8,15 +8,18 @@ import ( //go:generate mockery --name AuthInfoService --structname MockAuthInfoService --outpkg authinfotest --filename auth_info_service_mock.go --output ./authinfotest/ type AuthInfoService interface { GetAuthInfo(ctx context.Context, query *GetAuthInfoQuery) (*UserAuth, error) - GetUserLabels(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error) + GetUsersRecentlyUsedLabel(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error) + GetUserAuthModuleLabels(ctx context.Context, userID int64) ([]string, error) SetAuthInfo(ctx context.Context, cmd *SetAuthInfoCommand) error UpdateAuthInfo(ctx context.Context, cmd *UpdateAuthInfoCommand) error DeleteUserAuthInfo(ctx context.Context, userID int64) error } +//go:generate mockery --name Store --structname MockAuthInfoStore --outpkg authinfotest --filename auth_info_store_mock.go --output ./authinfotest/ type Store interface { GetAuthInfo(ctx context.Context, query *GetAuthInfoQuery) (*UserAuth, error) - GetUserLabels(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error) + GetUsersRecentlyUsedLabel(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error) + GetUserAuthModules(ctx context.Context, userID int64) ([]string, error) SetAuthInfo(ctx context.Context, cmd *SetAuthInfoCommand) error UpdateAuthInfo(ctx context.Context, cmd *UpdateAuthInfoCommand) error DeleteUserAuthInfo(ctx context.Context, userID int64) error diff --git a/pkg/services/login/authinfoimpl/service.go b/pkg/services/login/authinfoimpl/service.go index 1d7736c2c54..3c92e0c795f 100644 --- a/pkg/services/login/authinfoimpl/service.go +++ b/pkg/services/login/authinfoimpl/service.go @@ -67,11 +67,28 @@ func (s *Service) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery return authInfo, nil } -func (s *Service) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { +// GetUserAuthModuleLabels returns all auth modules for a user ordered by most recent first. +func (s *Service) GetUserAuthModuleLabels(ctx context.Context, userID int64) ([]string, error) { + modules, err := s.authInfoStore.GetUserAuthModules(ctx, userID) + if err != nil { + return nil, err + } + + result := make([]string, 0, len(modules)) + // modules should be unique and should not contain empty strings + for _, m := range modules { + label := login.GetAuthProviderLabel(m) + result = append(result, label) + } + + return result, nil +} + +func (s *Service) GetUsersRecentlyUsedLabel(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { if len(query.UserIDs) == 0 { return map[int64]string{}, nil } - return s.authInfoStore.GetUserLabels(ctx, query) + return s.authInfoStore.GetUsersRecentlyUsedLabel(ctx, query) } func (s *Service) setAuthInfoInCache(ctx context.Context, query *login.GetAuthInfoQuery, info *login.UserAuth) error { diff --git a/pkg/services/login/authinfoimpl/service_test.go b/pkg/services/login/authinfoimpl/service_test.go new file mode 100644 index 00000000000..210ac463625 --- /dev/null +++ b/pkg/services/login/authinfoimpl/service_test.go @@ -0,0 +1,31 @@ +package authinfoimpl + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfotest" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestAuthInfoService_GetUserAuthModuleLabels(t *testing.T) { + store := authinfotest.NewMockAuthInfoStore(t) + + userID := int64(42) + // Input modules from store (order matters, uniqueness assumed) + modules := []string{login.OktaAuthModule, login.LDAPAuthModule, login.SAMLAuthModule} + + store.On("GetUserAuthModules", mock.Anything, userID).Return(modules, nil) + + svc := ProvideService(store, nil, nil) + + actual, err := svc.GetUserAuthModuleLabels(context.Background(), userID) + require.NoError(t, err) + + expected := []string{login.GetAuthProviderLabel(login.OktaAuthModule), login.GetAuthProviderLabel(login.LDAPAuthModule), login.GetAuthProviderLabel(login.SAMLAuthModule)} + + // Verify labels mapped and order preserved + require.Equal(t, expected, actual) +} diff --git a/pkg/services/login/authinfoimpl/store.go b/pkg/services/login/authinfoimpl/store.go index 3d8dcd6c39f..49b1c565754 100644 --- a/pkg/services/login/authinfoimpl/store.go +++ b/pkg/services/login/authinfoimpl/store.go @@ -82,7 +82,7 @@ func (s *Store) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) return userAuth, nil } -func (s *Store) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { +func (s *Store) GetUsersRecentlyUsedLabel(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { userAuths := []login.UserAuth{} params := make([]interface{}, 0, len(query.UserIDs)) for _, id := range query.UserIDs { @@ -105,6 +105,29 @@ func (s *Store) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuer return labelMap, nil } +// GetUserAuthModules returns all auth modules a user has used ordered by most recently used first. +func (s *Store) GetUserAuthModules(ctx context.Context, userID int64) ([]string, error) { + rows := make([]struct { + AuthModule string `xorm:"auth_module"` + }, 0) + err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + return sess.Table("user_auth").Where("user_id = ?", userID).Desc("created").Cols("auth_module").Find(&rows) + }) + if err != nil { + return nil, err + } + modules := make([]string, 0, len(rows)) + seen := make(map[string]struct{}, len(rows)) + for _, r := range rows { + if _, ok := seen[r.AuthModule]; ok { + continue + } + seen[r.AuthModule] = struct{}{} + modules = append(modules, r.AuthModule) + } + return modules, nil +} + func (s *Store) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { authUser := &login.UserAuth{ UserId: cmd.UserId, diff --git a/pkg/services/login/authinfoimpl/store_test.go b/pkg/services/login/authinfoimpl/store_test.go index aa6f2b85083..a930580c1d7 100644 --- a/pkg/services/login/authinfoimpl/store_test.go +++ b/pkg/services/login/authinfoimpl/store_test.go @@ -45,7 +45,7 @@ func TestIntegrationAuthInfoStore(t *testing.T) { UserId: 2, })) - labels, err := store.GetUserLabels(ctx, login.GetUserLabelsQuery{UserIDs: []int64{1, 2}}) + labels, err := store.GetUsersRecentlyUsedLabel(ctx, login.GetUserLabelsQuery{UserIDs: []int64{1, 2}}) require.NoError(t, err) require.Len(t, labels, 2) diff --git a/pkg/services/login/authinfotest/auth_info_service_mock.go b/pkg/services/login/authinfotest/auth_info_service_mock.go index 42f9bd60b7f..a7004c24c54 100644 --- a/pkg/services/login/authinfotest/auth_info_service_mock.go +++ b/pkg/services/login/authinfotest/auth_info_service_mock.go @@ -1,17 +1,163 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify +// Code generated by mockery v2.53.5. DO NOT EDIT. package authinfotest import ( - "context" + context "context" - "github.com/grafana/grafana/pkg/services/login" - "github.com/grafana/grafana/pkg/services/user" + login "github.com/grafana/grafana/pkg/services/login" mock "github.com/stretchr/testify/mock" ) +// MockAuthInfoService is an autogenerated mock type for the AuthInfoService type +type MockAuthInfoService struct { + mock.Mock +} + +// DeleteUserAuthInfo provides a mock function with given fields: ctx, userID +func (_m *MockAuthInfoService) DeleteUserAuthInfo(ctx context.Context, userID int64) error { + ret := _m.Called(ctx, userID) + + if len(ret) == 0 { + panic("no return value specified for DeleteUserAuthInfo") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetAuthInfo provides a mock function with given fields: ctx, query +func (_m *MockAuthInfoService) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for GetAuthInfo") + } + + var r0 *login.UserAuth + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) (*login.UserAuth, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) *login.UserAuth); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*login.UserAuth) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *login.GetAuthInfoQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUserAuthModuleLabels provides a mock function with given fields: ctx, userID +func (_m *MockAuthInfoService) GetUserAuthModuleLabels(ctx context.Context, userID int64) ([]string, error) { + ret := _m.Called(ctx, userID) + + if len(ret) == 0 { + panic("no return value specified for GetUserAuthModuleLabels") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) ([]string, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) []string); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersRecentlyUsedLabel provides a mock function with given fields: ctx, query +func (_m *MockAuthInfoService) GetUsersRecentlyUsedLabel(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for GetUsersRecentlyUsedLabel") + } + + var r0 map[int64]string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) (map[int64]string, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) map[int64]string); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[int64]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, login.GetUserLabelsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetAuthInfo provides a mock function with given fields: ctx, cmd +func (_m *MockAuthInfoService) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { + ret := _m.Called(ctx, cmd) + + if len(ret) == 0 { + panic("no return value specified for SetAuthInfo") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *login.SetAuthInfoCommand) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateAuthInfo provides a mock function with given fields: ctx, cmd +func (_m *MockAuthInfoService) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { + ret := _m.Called(ctx, cmd) + + if len(ret) == 0 { + panic("no return value specified for UpdateAuthInfo") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *login.UpdateAuthInfoCommand) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NewMockAuthInfoService creates a new instance of MockAuthInfoService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockAuthInfoService(t interface { @@ -25,741 +171,3 @@ func NewMockAuthInfoService(t interface { return mock } - -// MockAuthInfoService is an autogenerated mock type for the AuthInfoService type -type MockAuthInfoService struct { - mock.Mock -} - -type MockAuthInfoService_Expecter struct { - mock *mock.Mock -} - -func (_m *MockAuthInfoService) EXPECT() *MockAuthInfoService_Expecter { - return &MockAuthInfoService_Expecter{mock: &_m.Mock} -} - -// DeleteUserAuthInfo provides a mock function for the type MockAuthInfoService -func (_mock *MockAuthInfoService) DeleteUserAuthInfo(ctx context.Context, userID int64) error { - ret := _mock.Called(ctx, userID) - - if len(ret) == 0 { - panic("no return value specified for DeleteUserAuthInfo") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, int64) error); ok { - r0 = returnFunc(ctx, userID) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockAuthInfoService_DeleteUserAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteUserAuthInfo' -type MockAuthInfoService_DeleteUserAuthInfo_Call struct { - *mock.Call -} - -// DeleteUserAuthInfo is a helper method to define mock.On call -// - ctx context.Context -// - userID int64 -func (_e *MockAuthInfoService_Expecter) DeleteUserAuthInfo(ctx interface{}, userID interface{}) *MockAuthInfoService_DeleteUserAuthInfo_Call { - return &MockAuthInfoService_DeleteUserAuthInfo_Call{Call: _e.mock.On("DeleteUserAuthInfo", ctx, userID)} -} - -func (_c *MockAuthInfoService_DeleteUserAuthInfo_Call) Run(run func(ctx context.Context, userID int64)) *MockAuthInfoService_DeleteUserAuthInfo_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 int64 - if args[1] != nil { - arg1 = args[1].(int64) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockAuthInfoService_DeleteUserAuthInfo_Call) Return(err error) *MockAuthInfoService_DeleteUserAuthInfo_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockAuthInfoService_DeleteUserAuthInfo_Call) RunAndReturn(run func(ctx context.Context, userID int64) error) *MockAuthInfoService_DeleteUserAuthInfo_Call { - _c.Call.Return(run) - return _c -} - -// GetAuthInfo provides a mock function for the type MockAuthInfoService -func (_mock *MockAuthInfoService) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error) { - ret := _mock.Called(ctx, query) - - if len(ret) == 0 { - panic("no return value specified for GetAuthInfo") - } - - var r0 *login.UserAuth - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) (*login.UserAuth, error)); ok { - return returnFunc(ctx, query) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) *login.UserAuth); ok { - r0 = returnFunc(ctx, query) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*login.UserAuth) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, *login.GetAuthInfoQuery) error); ok { - r1 = returnFunc(ctx, query) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockAuthInfoService_GetAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthInfo' -type MockAuthInfoService_GetAuthInfo_Call struct { - *mock.Call -} - -// GetAuthInfo is a helper method to define mock.On call -// - ctx context.Context -// - query *login.GetAuthInfoQuery -func (_e *MockAuthInfoService_Expecter) GetAuthInfo(ctx interface{}, query interface{}) *MockAuthInfoService_GetAuthInfo_Call { - return &MockAuthInfoService_GetAuthInfo_Call{Call: _e.mock.On("GetAuthInfo", ctx, query)} -} - -func (_c *MockAuthInfoService_GetAuthInfo_Call) Run(run func(ctx context.Context, query *login.GetAuthInfoQuery)) *MockAuthInfoService_GetAuthInfo_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 *login.GetAuthInfoQuery - if args[1] != nil { - arg1 = args[1].(*login.GetAuthInfoQuery) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockAuthInfoService_GetAuthInfo_Call) Return(userAuth *login.UserAuth, err error) *MockAuthInfoService_GetAuthInfo_Call { - _c.Call.Return(userAuth, err) - return _c -} - -func (_c *MockAuthInfoService_GetAuthInfo_Call) RunAndReturn(run func(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error)) *MockAuthInfoService_GetAuthInfo_Call { - _c.Call.Return(run) - return _c -} - -// GetUserLabels provides a mock function for the type MockAuthInfoService -func (_mock *MockAuthInfoService) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { - ret := _mock.Called(ctx, query) - - if len(ret) == 0 { - panic("no return value specified for GetUserLabels") - } - - var r0 map[int64]string - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) (map[int64]string, error)); ok { - return returnFunc(ctx, query) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) map[int64]string); ok { - r0 = returnFunc(ctx, query) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[int64]string) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, login.GetUserLabelsQuery) error); ok { - r1 = returnFunc(ctx, query) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockAuthInfoService_GetUserLabels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserLabels' -type MockAuthInfoService_GetUserLabels_Call struct { - *mock.Call -} - -// GetUserLabels is a helper method to define mock.On call -// - ctx context.Context -// - query login.GetUserLabelsQuery -func (_e *MockAuthInfoService_Expecter) GetUserLabels(ctx interface{}, query interface{}) *MockAuthInfoService_GetUserLabels_Call { - return &MockAuthInfoService_GetUserLabels_Call{Call: _e.mock.On("GetUserLabels", ctx, query)} -} - -func (_c *MockAuthInfoService_GetUserLabels_Call) Run(run func(ctx context.Context, query login.GetUserLabelsQuery)) *MockAuthInfoService_GetUserLabels_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 login.GetUserLabelsQuery - if args[1] != nil { - arg1 = args[1].(login.GetUserLabelsQuery) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockAuthInfoService_GetUserLabels_Call) Return(int64ToString map[int64]string, err error) *MockAuthInfoService_GetUserLabels_Call { - _c.Call.Return(int64ToString, err) - return _c -} - -func (_c *MockAuthInfoService_GetUserLabels_Call) RunAndReturn(run func(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error)) *MockAuthInfoService_GetUserLabels_Call { - _c.Call.Return(run) - return _c -} - -// SetAuthInfo provides a mock function for the type MockAuthInfoService -func (_mock *MockAuthInfoService) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { - ret := _mock.Called(ctx, cmd) - - if len(ret) == 0 { - panic("no return value specified for SetAuthInfo") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *login.SetAuthInfoCommand) error); ok { - r0 = returnFunc(ctx, cmd) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockAuthInfoService_SetAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAuthInfo' -type MockAuthInfoService_SetAuthInfo_Call struct { - *mock.Call -} - -// SetAuthInfo is a helper method to define mock.On call -// - ctx context.Context -// - cmd *login.SetAuthInfoCommand -func (_e *MockAuthInfoService_Expecter) SetAuthInfo(ctx interface{}, cmd interface{}) *MockAuthInfoService_SetAuthInfo_Call { - return &MockAuthInfoService_SetAuthInfo_Call{Call: _e.mock.On("SetAuthInfo", ctx, cmd)} -} - -func (_c *MockAuthInfoService_SetAuthInfo_Call) Run(run func(ctx context.Context, cmd *login.SetAuthInfoCommand)) *MockAuthInfoService_SetAuthInfo_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 *login.SetAuthInfoCommand - if args[1] != nil { - arg1 = args[1].(*login.SetAuthInfoCommand) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockAuthInfoService_SetAuthInfo_Call) Return(err error) *MockAuthInfoService_SetAuthInfo_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockAuthInfoService_SetAuthInfo_Call) RunAndReturn(run func(ctx context.Context, cmd *login.SetAuthInfoCommand) error) *MockAuthInfoService_SetAuthInfo_Call { - _c.Call.Return(run) - return _c -} - -// UpdateAuthInfo provides a mock function for the type MockAuthInfoService -func (_mock *MockAuthInfoService) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { - ret := _mock.Called(ctx, cmd) - - if len(ret) == 0 { - panic("no return value specified for UpdateAuthInfo") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *login.UpdateAuthInfoCommand) error); ok { - r0 = returnFunc(ctx, cmd) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockAuthInfoService_UpdateAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAuthInfo' -type MockAuthInfoService_UpdateAuthInfo_Call struct { - *mock.Call -} - -// UpdateAuthInfo is a helper method to define mock.On call -// - ctx context.Context -// - cmd *login.UpdateAuthInfoCommand -func (_e *MockAuthInfoService_Expecter) UpdateAuthInfo(ctx interface{}, cmd interface{}) *MockAuthInfoService_UpdateAuthInfo_Call { - return &MockAuthInfoService_UpdateAuthInfo_Call{Call: _e.mock.On("UpdateAuthInfo", ctx, cmd)} -} - -func (_c *MockAuthInfoService_UpdateAuthInfo_Call) Run(run func(ctx context.Context, cmd *login.UpdateAuthInfoCommand)) *MockAuthInfoService_UpdateAuthInfo_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 *login.UpdateAuthInfoCommand - if args[1] != nil { - arg1 = args[1].(*login.UpdateAuthInfoCommand) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockAuthInfoService_UpdateAuthInfo_Call) Return(err error) *MockAuthInfoService_UpdateAuthInfo_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockAuthInfoService_UpdateAuthInfo_Call) RunAndReturn(run func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error) *MockAuthInfoService_UpdateAuthInfo_Call { - _c.Call.Return(run) - return _c -} - -// NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockStore(t interface { - mock.TestingT - Cleanup(func()) -}) *MockStore { - mock := &MockStore{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// MockStore is an autogenerated mock type for the Store type -type MockStore struct { - mock.Mock -} - -type MockStore_Expecter struct { - mock *mock.Mock -} - -func (_m *MockStore) EXPECT() *MockStore_Expecter { - return &MockStore_Expecter{mock: &_m.Mock} -} - -// DeleteUserAuthInfo provides a mock function for the type MockStore -func (_mock *MockStore) DeleteUserAuthInfo(ctx context.Context, userID int64) error { - ret := _mock.Called(ctx, userID) - - if len(ret) == 0 { - panic("no return value specified for DeleteUserAuthInfo") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, int64) error); ok { - r0 = returnFunc(ctx, userID) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockStore_DeleteUserAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteUserAuthInfo' -type MockStore_DeleteUserAuthInfo_Call struct { - *mock.Call -} - -// DeleteUserAuthInfo is a helper method to define mock.On call -// - ctx context.Context -// - userID int64 -func (_e *MockStore_Expecter) DeleteUserAuthInfo(ctx interface{}, userID interface{}) *MockStore_DeleteUserAuthInfo_Call { - return &MockStore_DeleteUserAuthInfo_Call{Call: _e.mock.On("DeleteUserAuthInfo", ctx, userID)} -} - -func (_c *MockStore_DeleteUserAuthInfo_Call) Run(run func(ctx context.Context, userID int64)) *MockStore_DeleteUserAuthInfo_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 int64 - if args[1] != nil { - arg1 = args[1].(int64) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockStore_DeleteUserAuthInfo_Call) Return(err error) *MockStore_DeleteUserAuthInfo_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockStore_DeleteUserAuthInfo_Call) RunAndReturn(run func(ctx context.Context, userID int64) error) *MockStore_DeleteUserAuthInfo_Call { - _c.Call.Return(run) - return _c -} - -// GetAuthInfo provides a mock function for the type MockStore -func (_mock *MockStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error) { - ret := _mock.Called(ctx, query) - - if len(ret) == 0 { - panic("no return value specified for GetAuthInfo") - } - - var r0 *login.UserAuth - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) (*login.UserAuth, error)); ok { - return returnFunc(ctx, query) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) *login.UserAuth); ok { - r0 = returnFunc(ctx, query) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*login.UserAuth) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, *login.GetAuthInfoQuery) error); ok { - r1 = returnFunc(ctx, query) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockStore_GetAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthInfo' -type MockStore_GetAuthInfo_Call struct { - *mock.Call -} - -// GetAuthInfo is a helper method to define mock.On call -// - ctx context.Context -// - query *login.GetAuthInfoQuery -func (_e *MockStore_Expecter) GetAuthInfo(ctx interface{}, query interface{}) *MockStore_GetAuthInfo_Call { - return &MockStore_GetAuthInfo_Call{Call: _e.mock.On("GetAuthInfo", ctx, query)} -} - -func (_c *MockStore_GetAuthInfo_Call) Run(run func(ctx context.Context, query *login.GetAuthInfoQuery)) *MockStore_GetAuthInfo_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 *login.GetAuthInfoQuery - if args[1] != nil { - arg1 = args[1].(*login.GetAuthInfoQuery) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockStore_GetAuthInfo_Call) Return(userAuth *login.UserAuth, err error) *MockStore_GetAuthInfo_Call { - _c.Call.Return(userAuth, err) - return _c -} - -func (_c *MockStore_GetAuthInfo_Call) RunAndReturn(run func(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error)) *MockStore_GetAuthInfo_Call { - _c.Call.Return(run) - return _c -} - -// GetUserLabels provides a mock function for the type MockStore -func (_mock *MockStore) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { - ret := _mock.Called(ctx, query) - - if len(ret) == 0 { - panic("no return value specified for GetUserLabels") - } - - var r0 map[int64]string - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) (map[int64]string, error)); ok { - return returnFunc(ctx, query) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) map[int64]string); ok { - r0 = returnFunc(ctx, query) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[int64]string) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, login.GetUserLabelsQuery) error); ok { - r1 = returnFunc(ctx, query) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockStore_GetUserLabels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserLabels' -type MockStore_GetUserLabels_Call struct { - *mock.Call -} - -// GetUserLabels is a helper method to define mock.On call -// - ctx context.Context -// - query login.GetUserLabelsQuery -func (_e *MockStore_Expecter) GetUserLabels(ctx interface{}, query interface{}) *MockStore_GetUserLabels_Call { - return &MockStore_GetUserLabels_Call{Call: _e.mock.On("GetUserLabels", ctx, query)} -} - -func (_c *MockStore_GetUserLabels_Call) Run(run func(ctx context.Context, query login.GetUserLabelsQuery)) *MockStore_GetUserLabels_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 login.GetUserLabelsQuery - if args[1] != nil { - arg1 = args[1].(login.GetUserLabelsQuery) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockStore_GetUserLabels_Call) Return(int64ToString map[int64]string, err error) *MockStore_GetUserLabels_Call { - _c.Call.Return(int64ToString, err) - return _c -} - -func (_c *MockStore_GetUserLabels_Call) RunAndReturn(run func(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error)) *MockStore_GetUserLabels_Call { - _c.Call.Return(run) - return _c -} - -// SetAuthInfo provides a mock function for the type MockStore -func (_mock *MockStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { - ret := _mock.Called(ctx, cmd) - - if len(ret) == 0 { - panic("no return value specified for SetAuthInfo") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *login.SetAuthInfoCommand) error); ok { - r0 = returnFunc(ctx, cmd) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockStore_SetAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAuthInfo' -type MockStore_SetAuthInfo_Call struct { - *mock.Call -} - -// SetAuthInfo is a helper method to define mock.On call -// - ctx context.Context -// - cmd *login.SetAuthInfoCommand -func (_e *MockStore_Expecter) SetAuthInfo(ctx interface{}, cmd interface{}) *MockStore_SetAuthInfo_Call { - return &MockStore_SetAuthInfo_Call{Call: _e.mock.On("SetAuthInfo", ctx, cmd)} -} - -func (_c *MockStore_SetAuthInfo_Call) Run(run func(ctx context.Context, cmd *login.SetAuthInfoCommand)) *MockStore_SetAuthInfo_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 *login.SetAuthInfoCommand - if args[1] != nil { - arg1 = args[1].(*login.SetAuthInfoCommand) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockStore_SetAuthInfo_Call) Return(err error) *MockStore_SetAuthInfo_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockStore_SetAuthInfo_Call) RunAndReturn(run func(ctx context.Context, cmd *login.SetAuthInfoCommand) error) *MockStore_SetAuthInfo_Call { - _c.Call.Return(run) - return _c -} - -// UpdateAuthInfo provides a mock function for the type MockStore -func (_mock *MockStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { - ret := _mock.Called(ctx, cmd) - - if len(ret) == 0 { - panic("no return value specified for UpdateAuthInfo") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *login.UpdateAuthInfoCommand) error); ok { - r0 = returnFunc(ctx, cmd) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockStore_UpdateAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAuthInfo' -type MockStore_UpdateAuthInfo_Call struct { - *mock.Call -} - -// UpdateAuthInfo is a helper method to define mock.On call -// - ctx context.Context -// - cmd *login.UpdateAuthInfoCommand -func (_e *MockStore_Expecter) UpdateAuthInfo(ctx interface{}, cmd interface{}) *MockStore_UpdateAuthInfo_Call { - return &MockStore_UpdateAuthInfo_Call{Call: _e.mock.On("UpdateAuthInfo", ctx, cmd)} -} - -func (_c *MockStore_UpdateAuthInfo_Call) Run(run func(ctx context.Context, cmd *login.UpdateAuthInfoCommand)) *MockStore_UpdateAuthInfo_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 *login.UpdateAuthInfoCommand - if args[1] != nil { - arg1 = args[1].(*login.UpdateAuthInfoCommand) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockStore_UpdateAuthInfo_Call) Return(err error) *MockStore_UpdateAuthInfo_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockStore_UpdateAuthInfo_Call) RunAndReturn(run func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error) *MockStore_UpdateAuthInfo_Call { - _c.Call.Return(run) - return _c -} - -// NewMockUserProtectionService creates a new instance of MockUserProtectionService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockUserProtectionService(t interface { - mock.TestingT - Cleanup(func()) -}) *MockUserProtectionService { - mock := &MockUserProtectionService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// MockUserProtectionService is an autogenerated mock type for the UserProtectionService type -type MockUserProtectionService struct { - mock.Mock -} - -type MockUserProtectionService_Expecter struct { - mock *mock.Mock -} - -func (_m *MockUserProtectionService) EXPECT() *MockUserProtectionService_Expecter { - return &MockUserProtectionService_Expecter{mock: &_m.Mock} -} - -// AllowUserMapping provides a mock function for the type MockUserProtectionService -func (_mock *MockUserProtectionService) AllowUserMapping(user1 *user.User, authModule string) error { - ret := _mock.Called(user1, authModule) - - if len(ret) == 0 { - panic("no return value specified for AllowUserMapping") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(*user.User, string) error); ok { - r0 = returnFunc(user1, authModule) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockUserProtectionService_AllowUserMapping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AllowUserMapping' -type MockUserProtectionService_AllowUserMapping_Call struct { - *mock.Call -} - -// AllowUserMapping is a helper method to define mock.On call -// - user1 *user.User -// - authModule string -func (_e *MockUserProtectionService_Expecter) AllowUserMapping(user1 interface{}, authModule interface{}) *MockUserProtectionService_AllowUserMapping_Call { - return &MockUserProtectionService_AllowUserMapping_Call{Call: _e.mock.On("AllowUserMapping", user1, authModule)} -} - -func (_c *MockUserProtectionService_AllowUserMapping_Call) Run(run func(user1 *user.User, authModule string)) *MockUserProtectionService_AllowUserMapping_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 *user.User - if args[0] != nil { - arg0 = args[0].(*user.User) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockUserProtectionService_AllowUserMapping_Call) Return(err error) *MockUserProtectionService_AllowUserMapping_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockUserProtectionService_AllowUserMapping_Call) RunAndReturn(run func(user1 *user.User, authModule string) error) *MockUserProtectionService_AllowUserMapping_Call { - _c.Call.Return(run) - return _c -} diff --git a/pkg/services/login/authinfotest/auth_info_store_mock.go b/pkg/services/login/authinfotest/auth_info_store_mock.go new file mode 100644 index 00000000000..676985404c8 --- /dev/null +++ b/pkg/services/login/authinfotest/auth_info_store_mock.go @@ -0,0 +1,173 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package authinfotest + +import ( + context "context" + + login "github.com/grafana/grafana/pkg/services/login" + mock "github.com/stretchr/testify/mock" +) + +// MockAuthInfoStore is an autogenerated mock type for the Store type +type MockAuthInfoStore struct { + mock.Mock +} + +// DeleteUserAuthInfo provides a mock function with given fields: ctx, userID +func (_m *MockAuthInfoStore) DeleteUserAuthInfo(ctx context.Context, userID int64) error { + ret := _m.Called(ctx, userID) + + if len(ret) == 0 { + panic("no return value specified for DeleteUserAuthInfo") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetAuthInfo provides a mock function with given fields: ctx, query +func (_m *MockAuthInfoStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for GetAuthInfo") + } + + var r0 *login.UserAuth + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) (*login.UserAuth, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) *login.UserAuth); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*login.UserAuth) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *login.GetAuthInfoQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUserAuthModules provides a mock function with given fields: ctx, userID +func (_m *MockAuthInfoStore) GetUserAuthModules(ctx context.Context, userID int64) ([]string, error) { + ret := _m.Called(ctx, userID) + + if len(ret) == 0 { + panic("no return value specified for GetUserAuthModules") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) ([]string, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) []string); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersRecentlyUsedLabel provides a mock function with given fields: ctx, query +func (_m *MockAuthInfoStore) GetUsersRecentlyUsedLabel(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for GetUsersRecentlyUsedLabel") + } + + var r0 map[int64]string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) (map[int64]string, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) map[int64]string); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[int64]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, login.GetUserLabelsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetAuthInfo provides a mock function with given fields: ctx, cmd +func (_m *MockAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { + ret := _m.Called(ctx, cmd) + + if len(ret) == 0 { + panic("no return value specified for SetAuthInfo") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *login.SetAuthInfoCommand) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateAuthInfo provides a mock function with given fields: ctx, cmd +func (_m *MockAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error { + ret := _m.Called(ctx, cmd) + + if len(ret) == 0 { + panic("no return value specified for UpdateAuthInfo") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *login.UpdateAuthInfoCommand) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMockAuthInfoStore creates a new instance of MockAuthInfoStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockAuthInfoStore(t interface { + mock.TestingT + Cleanup(func()) +}) *MockAuthInfoStore { + mock := &MockAuthInfoStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/login/authinfotest/fake.go b/pkg/services/login/authinfotest/fake.go index 9a4d7843a3c..63a9796bbfc 100644 --- a/pkg/services/login/authinfotest/fake.go +++ b/pkg/services/login/authinfotest/fake.go @@ -8,11 +8,12 @@ import ( type FakeService struct { login.AuthInfoService - LatestUserID int64 - ExpectedUserAuth *login.UserAuth - ExpectedExternalUser *login.ExternalUserInfo - ExpectedError error - ExpectedLabels map[int64]string + LatestUserID int64 + ExpectedUserAuth *login.UserAuth + ExpectedExternalUser *login.ExternalUserInfo + ExpectedError error + ExpectedRecentlyUsedLabel map[int64]string + ExpectedAuthModuleLabels []string SetAuthInfoFn func(ctx context.Context, cmd *login.SetAuthInfoCommand) error UpdateAuthInfoFn func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error @@ -24,8 +25,12 @@ func (a *FakeService) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQ return a.ExpectedUserAuth, a.ExpectedError } -func (a *FakeService) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { - return a.ExpectedLabels, a.ExpectedError +func (a *FakeService) GetUsersRecentlyUsedLabel(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) { + return a.ExpectedRecentlyUsedLabel, a.ExpectedError +} + +func (a *FakeService) GetUserAuthModuleLabels(ctx context.Context, userID int64) ([]string, error) { + return a.ExpectedAuthModuleLabels, a.ExpectedError } func (a *FakeService) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error { diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index 51bbdf261b1..6f6ddd06e4c 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -11,7 +11,7 @@ const ( // are negative to ensure that the default items are placed above // any items with default weight. - WeightHome = (iota - 20) * 100 + WeightHome = (iota - 40) * 100 WeightBookmarks WeightSavedItems WeightDashboard @@ -21,12 +21,13 @@ const ( WeightAlerting WeightAlertsAndIncidents WeightAIAndML + WeightAdaptiveTelemetry + WeightCMAB WeightTestingAndSynthetics WeightObservability WeightCloudServiceProviders WeightInfrastructure WeightApplication - WeightFrontend WeightAsserts WeightDataConnections WeightApps @@ -48,7 +49,6 @@ const ( NavIDAlerting = "alerting" NavIDObservability = "observability" NavIDInfrastructure = "infrastructure" - NavIDFrontend = "frontend" NavIDReporting = "reports" NavIDApps = "apps" NavIDCfgGeneral = "cfg/general" @@ -158,77 +158,6 @@ func Sort(nodes []*NavLink) { } } -func (root *NavTreeRoot) ApplyHelpVersion(version string) { - helpNode := root.FindById("help") - - if helpNode != nil { - helpNode.SubTitle = version - } -} - -func (root *NavTreeRoot) ApplyCostManagementIA() { - orgAdminNode := root.FindById(NavIDCfg) - var costManagementApp *NavLink - var adaptiveMetricsApp *NavLink - var adaptiveLogsApp *NavLink - var adaptiveTracesApp *NavLink - var attributionsApp *NavLink - var logVolumeExplorerApp *NavLink - - if orgAdminNode != nil { - adminNodeLinks := []*NavLink{} - for _, element := range orgAdminNode.Children { - switch navId := element.Id; navId { - case "plugin-page-grafana-costmanagementui-app": - costManagementApp = element - case "plugin-page-grafana-adaptive-metrics-app": - adaptiveMetricsApp = element - case "plugin-page-grafana-adaptivelogs-app": - adaptiveLogsApp = element - case "plugin-page-grafana-adaptivetraces-app": - adaptiveTracesApp = element - case "plugin-page-grafana-attributions-app": - attributionsApp = element - case "plugin-page-grafana-logvolumeexplorer-app": - logVolumeExplorerApp = element - default: - adminNodeLinks = append(adminNodeLinks, element) - } - } - - if costManagementApp != nil { - costManagementMetricsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/metrics") - if costManagementMetricsNode != nil { - if adaptiveMetricsApp != nil { - costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, adaptiveMetricsApp) - } - if attributionsApp != nil { - costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, attributionsApp) - } - } - - costManagementLogsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/logs") - if costManagementLogsNode != nil { - if adaptiveLogsApp != nil { - costManagementLogsNode.Children = append(costManagementLogsNode.Children, adaptiveLogsApp) - } - if logVolumeExplorerApp != nil { - costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerApp) - } - } - - costManagementTracesNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/traces") - if costManagementTracesNode != nil { - if adaptiveTracesApp != nil { - costManagementTracesNode.Children = append(costManagementTracesNode.Children, adaptiveTracesApp) - } - } - adminNodeLinks = append(adminNodeLinks, costManagementApp) - } - orgAdminNode.Children = adminNodeLinks - } -} - func AppendIfNotNil(children []*NavLink, newChild *NavLink) []*NavLink { if newChild != nil { return append(children, newChild) diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 80fd5b0b542..02e9f44bdbd 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -56,7 +56,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink if c.HasRole(identity.RoleAdmin) && (s.cfg.StackID == "" || // show OnPrem even when provisioning is disabled s.features.IsEnabledGlobally(featuremgmt.FlagProvisioning)) { - configNodes = append(configNodes, &navtree.NavLink{ + generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ Text: "Provisioning", Id: "provisioning", SubTitle: "View and manage your provisioning connections", diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 8c92bad85af..d6650c85ee1 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -4,6 +4,7 @@ import ( "path" "sort" "strconv" + "strings" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -260,10 +261,22 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n } } + sectionChildren := []*navtree.NavLink{appLink} + // asserts pages expand to root Observability section instead of it's own node + if plugin.ID == "grafana-asserts-app" { + sectionChildren = appLink.Children + + // keep current sorting if the pages, but above all the other apps + for _, child := range sectionChildren { + child.SortWeight = -100 + child.SortWeight + child.Id = "standalone-plugin-page-" + strings.ReplaceAll(strings.ToLower(child.Text), " ", "-") + } + } + if sectionID == navtree.NavIDRoot { treeRoot.AddSection(appLink) } else if navNode := treeRoot.FindById(sectionID); navNode != nil { - navNode.Children = append(navNode.Children, appLink) + navNode.Children = append(navNode.Children, sectionChildren...) } else { switch sectionID { case navtree.NavIDApps: @@ -272,7 +285,7 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n Icon: "layer-group", SubTitle: "App plugins that extend the Grafana experience", Id: navtree.NavIDApps, - Children: []*navtree.NavLink{appLink}, + Children: sectionChildren, SortWeight: navtree.WeightApps, Url: s.cfg.AppSubURL + "/apps", }) @@ -283,7 +296,7 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n SubTitle: "Monitor infrastructure and applications in real time with Grafana Cloud's fully managed observability suite", Icon: "heart-rate", SortWeight: navtree.WeightObservability, - Children: []*navtree.NavLink{appLink}, + Children: sectionChildren, Url: s.cfg.AppSubURL + "/observability", }) case navtree.NavIDInfrastructure: @@ -293,19 +306,9 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n SubTitle: "Understand your infrastructure's health", Icon: "heart-rate", SortWeight: navtree.WeightInfrastructure, - Children: []*navtree.NavLink{appLink}, + Children: sectionChildren, Url: s.cfg.AppSubURL + "/infrastructure", }) - case navtree.NavIDFrontend: - treeRoot.AddSection(&navtree.NavLink{ - Text: "Frontend", - Id: navtree.NavIDFrontend, - SubTitle: "Gain real user monitoring insights", - Icon: "frontend-observability", - SortWeight: navtree.WeightFrontend, - Children: []*navtree.NavLink{appLink}, - Url: s.cfg.AppSubURL + "/frontend", - }) case navtree.NavIDAlertsAndIncidents: alertsAndIncidentsChildren := []*navtree.NavLink{} for _, alertingNode := range alertingNodes { @@ -332,7 +335,7 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n SubTitle: "Optimize performance with k6 and Synthetic Monitoring insights", Icon: "k6", SortWeight: navtree.WeightTestingAndSynthetics, - Children: []*navtree.NavLink{appLink}, + Children: sectionChildren, Url: s.cfg.AppSubURL + "/testing-and-synthetics", }) case navtree.NavIDAdaptiveTelemetry: @@ -341,8 +344,8 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n Id: navtree.NavIDAdaptiveTelemetry, SubTitle: "Reduce noise, cut costs, and accelerate troubleshooting by intelligently ingesting only the telemetry data that matters most.", Icon: "adaptive-telemetry", - SortWeight: navtree.WeightAIAndML + 1, // Place under "AI & Machine Learning" - Children: []*navtree.NavLink{appLink}, + SortWeight: navtree.WeightAdaptiveTelemetry, + Children: sectionChildren, Url: "adaptive-telemetry", // Use the icon URL from the first "Adaptive Telemetry" plugin in the list (they will all be the same) Img: s.cfg.AppSubURL + plugin.Info.Logos.Large, @@ -389,13 +392,11 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-ml-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightAIAndML, Text: "Machine Learning", SubTitle: "Explore AI and machine learning features", Icon: "gf-ml-alt"}, "grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 7}, "grafana-cloud-link-app": {SectionID: navtree.NavIDCfgPlugins, SortWeight: 3}, - "grafana-costmanagementui-app": {SectionID: navtree.NavIDCfg, Text: "Cost management"}, "grafana-adaptive-metrics-app": {SectionID: navtree.NavIDAdaptiveTelemetry, SortWeight: 1}, "grafana-adaptivelogs-app": {SectionID: navtree.NavIDAdaptiveTelemetry, SortWeight: 2}, "grafana-adaptivetraces-app": {SectionID: navtree.NavIDAdaptiveTelemetry, SortWeight: 3}, "grafana-adaptiveprofiles-app": {SectionID: navtree.NavIDAdaptiveTelemetry, SortWeight: 4}, - "grafana-attributions-app": {SectionID: navtree.NavIDCfg, Text: "Attributions"}, - "grafana-logvolumeexplorer-app": {SectionID: navtree.NavIDCfg, Text: "Log Volume Explorer"}, + "grafana-cmab-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightCMAB, Icon: "cmab-logo", IsNew: true}, "grafana-easystart-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApps + 1, Text: "Connections", Icon: "adjust-circle"}, "k6-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 1, Text: "Performance"}, } diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 955c0b32ce1..31fe4d2982f 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -443,7 +443,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingTriage) { if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{ - Text: "Triage", SubTitle: "Triage alerts", Id: "alert-triage", Url: s.cfg.AppSubURL + "/alerting/triage", Icon: "medkit", IsNew: true, + Text: "Alerts", SubTitle: "Visualize active and pending alerts", Id: "alert-alerts", Url: s.cfg.AppSubURL + "/alerting/alerts", Icon: "bell", IsNew: true, }) } } diff --git a/pkg/services/ngalert/models/receivers.go b/pkg/services/ngalert/models/receivers.go index 1130711da90..1601ddc94e5 100644 --- a/pkg/services/ngalert/models/receivers.go +++ b/pkg/services/ngalert/models/receivers.go @@ -130,6 +130,9 @@ func (r *Receiver) WithExistingSecureFields(existing *Receiver, integrationSecur // Validate validates all integration settings, ensuring that the integrations are correctly configured. func (r *Receiver) Validate(decryptFn DecryptFn) error { var errs []error + if r.Name == "" { + errs = append(errs, fmt.Errorf("name should not be an empty string")) + } for _, integration := range r.Integrations { if err := integration.Validate(decryptFn); err != nil { errs = append(errs, err) diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 88898de2d64..6de3f7b1ba0 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -10,11 +10,12 @@ import ( notificationHistorian "github.com/grafana/alerting/notify/historian" "github.com/grafana/alerting/notify/historian/lokiclient" "github.com/grafana/alerting/notify/nfstatus" - "github.com/grafana/grafana/pkg/services/ngalert/lokiconfig" "github.com/prometheus/alertmanager/featurecontrol" "github.com/prometheus/alertmanager/matchers/compat" "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/services/ngalert/lokiconfig" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" @@ -219,8 +220,8 @@ func (ng *AlertNG) init() error { SmtpConfig: smtpCfg, Timeout: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Timeout, } - autogenFn := func(ctx context.Context, logger log.Logger, orgID int64, cfg *definitions.PostableApiAlertingConfig, skipInvalid bool) error { - return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, skipInvalid, ng.FeatureToggles) + autogenFn := func(ctx context.Context, logger log.Logger, orgID int64, cfg *definitions.PostableApiAlertingConfig, invalidReceiverAction notifier.InvalidReceiversAction) error { + return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, invalidReceiverAction, ng.FeatureToggles) } // This function will be used by the MOA to create new Alertmanagers. @@ -503,13 +504,6 @@ func initInstanceStore(sqlStore db.DB, logger log.Logger, featureToggles feature if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStateCompressed) { logger.Info("Using protobuf-based alert instance store") instanceStore = protoInstanceStore - // If FlagAlertingSaveStateCompressed is enabled, ProtoInstanceDBStore is used, - // which functions differently from InstanceDBStore. FlagAlertingSaveStatePeriodic is - // not applicable to ProtoInstanceDBStore, so a warning is logged if it is set. - //nolint:staticcheck // not yet migrated to OpenFeature - if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStatePeriodic) { - logger.Warn("alertingSaveStatePeriodic is not used when alertingSaveStateCompressed feature flag enabled") - } } else { logger.Info("Using simple database alert instance store") instanceStore = simpleInstanceStore @@ -524,7 +518,15 @@ func initStatePersister(uaCfg setting.UnifiedAlertingSettings, cfg state.Manager //nolint:staticcheck // not yet migrated to OpenFeature if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStateCompressed) { logger.Info("Using rule state persister") - statePersister = state.NewSyncRuleStatePersisiter(logger, cfg) + + if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStatePeriodic) { + logger.Info("Compressed storage with periodic save enabled") + ticker := clock.New().Ticker(cfg.StatePeriodicSaveInterval) + statePersister = state.NewSyncRuleStatePersisiter(logger, ticker, cfg) + } else { + logger.Info("Compressed storage FullSync disabled") + statePersister = state.NewSyncRuleStatePersisiter(logger, nil, cfg) + } } else if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStatePeriodic) { logger.Info("Using periodic state persister") ticker := clock.New().Ticker(uaCfg.StatePeriodicSaveInterval) @@ -667,6 +669,8 @@ func configureHistorianBackend( if err != nil { return nil, fmt.Errorf("invalid remote loki configuration: %w", err) } + // Use external labels from state history config + lcfg.ExternalLabels = cfg.ExternalLabels req := lokiclient.NewRequester() logCtx := log.WithContextualAttributes(ctx, []any{"backend", "loki"}) lokiBackendLogger := log.New("ngalert.state.historian").FromContext(logCtx) diff --git a/pkg/services/ngalert/ngalert_test.go b/pkg/services/ngalert/ngalert_test.go index 640a81821c9..ef81499cd77 100644 --- a/pkg/services/ngalert/ngalert_test.go +++ b/pkg/services/ngalert/ngalert_test.go @@ -3,10 +3,17 @@ package ngalert import ( "bytes" "context" + "io" "math/rand" + "net/http" + "net/http/httptest" "testing" "time" + "github.com/gogo/protobuf/proto" + "github.com/golang/snappy" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/loki/pkg/push" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" @@ -20,9 +27,11 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/setting" @@ -151,6 +160,80 @@ func TestConfigureHistorianBackend(t *testing.T) { require.NoError(t, err) }) + t.Run("Loki backend sends external labels in Record calls", func(t *testing.T) { + var receivedRequest *http.Request + var receivedBody []byte + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedRequest = r + body, _ := io.ReadAll(r.Body) + receivedBody = body + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) + logger := log.NewNopLogger() + tracer := tracing.InitializeTracerForTest() + cfg := setting.UnifiedAlertingStateHistorySettings{ + Enabled: true, + Backend: "loki", + LokiSettings: setting.UnifiedAlertingLokiSettings{ + LokiReadURL: server.URL, + LokiWriteURL: server.URL, + }, + ExternalLabels: map[string]string{ + "test_label": "test_value", + "cluster": "prod", + }, + } + ac := &acfakes.FakeRuleService{} + + h, err := configureHistorianBackend(context.Background(), cfg, nil, nil, nil, met, logger, tracer, ac, nil, nil, nil, nil, nil) + require.NoError(t, err) + require.NotNil(t, h) + + rule := history_model.RuleMeta{ + OrgID: 1, + UID: "test-rule-uid", + Group: "test-group", + NamespaceUID: "test-namespace", + Title: "Test Rule", + } + states := []state.StateTransition{ + { + PreviousState: eval.Normal, + State: &state.State{ + State: eval.Alerting, + Labels: data.Labels{"instance": "test-instance"}, + LastEvaluationTime: time.Now(), + }, + }, + } + + errCh := h.Record(context.Background(), rule, states) + err = <-errCh + require.NoError(t, err) + + require.NotNil(t, receivedRequest, "Expected HTTP request to be sent to Loki") + require.Contains(t, receivedRequest.URL.Path, "/loki/api/v1/push") + + // Loki uses snappy-compressed protobuf encoding + decompressed, err := snappy.Decode(nil, receivedBody) + require.NoError(t, err) + + var req push.PushRequest + err = proto.Unmarshal(decompressed, &req) + require.NoError(t, err) + + require.Len(t, req.Streams, 1, "Expected exactly one stream") + stream := req.Streams[0] + + require.Contains(t, stream.Labels, `test_label="test_value"`) + require.Contains(t, stream.Labels, `cluster="prod"`) + require.Contains(t, stream.Labels, `from="state-history"`) + require.Contains(t, stream.Labels, `orgID="1"`) + }) + t.Run("fail initialization if prometheus backend missing datasource UID", func(t *testing.T) { met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) logger := log.NewNopLogger() @@ -353,7 +436,9 @@ func TestInitStatePersister(t *testing.T) { ua := setting.UnifiedAlertingSettings{ StatePeriodicSaveInterval: 1 * time.Minute, } - cfg := state.ManagerCfg{} + cfg := state.ManagerCfg{ + StatePeriodicSaveInterval: 1 * time.Minute, + } tests := []struct { name string diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index f4454d87c5f..05e42a66b09 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -194,7 +194,7 @@ func (am *alertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error { } err = am.Store.SaveAlertmanagerConfigurationWithCallback(ctx, cmd, func() error { - _, err = am.applyConfig(ctx, cfg, true) + _, err = am.applyConfig(ctx, cfg, LogInvalidReceivers) return err }) if err != nil { @@ -233,7 +233,7 @@ func (am *alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P } err = am.Store.SaveAlertmanagerConfigurationWithCallback(ctx, cmd, func() error { - _, err = am.applyConfig(ctx, cfg, false) // fail if the autogen config is invalid + _, err = am.applyConfig(ctx, cfg, ErrorOnInvalidReceivers) // fail if the autogen config is invalid return err }) if err != nil { @@ -259,7 +259,7 @@ func (am *alertmanager) ApplyConfig(ctx context.Context, dbCfg *ngmodels.AlertCo // Since we will now update last_applied when autogen changes even if the user-created config remains the same. // To fix this however, the local alertmanager needs to be able to tell the difference between user-created and // autogen config, which may introduce cross-cutting complexity. - configChanged, err := am.applyConfig(ctx, cfg, true) + configChanged, err := am.applyConfig(ctx, cfg, LogInvalidReceivers) if err != nil { outerErr = fmt.Errorf("unable to apply configuration: %w", err) return @@ -330,7 +330,7 @@ func (am *alertmanager) aggregateInhibitMatchers(rules []config.InhibitRule, amu // applyConfig applies a new configuration by re-initializing all components using the configuration provided. // It returns a boolean indicating whether the user config was changed and an error. // It is not safe to call concurrently. -func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.PostableUserConfig, skipInvalid bool) (bool, error) { +func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.PostableUserConfig, onInvalid InvalidReceiversAction) (bool, error) { err := am.crypto.DecryptExtraConfigs(ctx, cfg) if err != nil { return false, fmt.Errorf("failed to decrypt external configurations: %w", err) @@ -347,7 +347,7 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable templates := alertingNotify.PostableAPITemplatesToTemplateDefinitions(cfg.GetMergedTemplateDefinitions()) // Now add autogenerated config to the route. - err = AddAutogenConfig(ctx, am.logger, am.Store, am.Base.TenantID(), &amConfig, skipInvalid, am.features) + err = AddAutogenConfig(ctx, am.logger, am.Store, am.Base.TenantID(), &amConfig, onInvalid, am.features) if err != nil { return false, err } diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go index 514da9be686..87d573124e7 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config.go +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -133,7 +133,7 @@ func (moa *MultiOrgAlertmanager) GetAlertmanagerConfiguration(ctx context.Contex // Otherwise, broken settings (e.g. a receiver that doesn't exist) will cause the config returned here to be // different than the config currently in-use. // TODO: Preferably, we'd be getting the config directly from the in-memory AM so adding the autogen config would not be necessary. - err := AddAutogenConfig(ctx, moa.logger, moa.configStore, org, &cfg.AlertmanagerConfig, true, moa.featureManager) + err := AddAutogenConfig(ctx, moa.logger, moa.configStore, org, &cfg.AlertmanagerConfig, LogInvalidReceivers, moa.featureManager) if err != nil { return definitions.GettableUserConfig{}, err } diff --git a/pkg/services/ngalert/notifier/autogen_alertmanager.go b/pkg/services/ngalert/notifier/autogen_alertmanager.go index 7523e368ad5..f0a3ae040ee 100644 --- a/pkg/services/ngalert/notifier/autogen_alertmanager.go +++ b/pkg/services/ngalert/notifier/autogen_alertmanager.go @@ -21,10 +21,18 @@ type autogenRuleStore interface { ListNotificationSettings(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) } +type InvalidReceiversAction string + +const ( + ErrorOnInvalidReceivers InvalidReceiversAction = "error" + LogInvalidReceivers InvalidReceiversAction = "log" + IgnoreInvalidReceivers InvalidReceiversAction = "ignore" +) + // AddAutogenConfig creates the autogenerated configuration and adds it to the given apiAlertingConfig. // If skipInvalid is true, then invalid notification settings are skipped, otherwise an error is returned. -func AddAutogenConfig[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], skipInvalid bool, features featuremgmt.FeatureToggles) error { - autogenRoute, err := newAutogeneratedRoute(ctx, logger, store, orgId, cfg, skipInvalid, features) +func AddAutogenConfig[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], invalidReceiverAction InvalidReceiversAction, features featuremgmt.FeatureToggles) error { + autogenRoute, err := newAutogeneratedRoute(ctx, logger, store, orgId, cfg, invalidReceiverAction, features) if err != nil { return err } @@ -40,7 +48,7 @@ func AddAutogenConfig[R receiver](ctx context.Context, logger log.Logger, store // newAutogeneratedRoute creates a new autogenerated route based on the notification settings for the given org. // cfg is used to construct the settings validator and to ensure we create a dedicated route for each receiver. // skipInvalid is used to skip invalid settings instead of returning an error. -func newAutogeneratedRoute[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], skipInvalid bool, features featuremgmt.FeatureToggles) (autogeneratedRoute, error) { +func newAutogeneratedRoute[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], invalidReceiverAction InvalidReceiversAction, features featuremgmt.FeatureToggles) (autogeneratedRoute, error) { settings, err := store.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgId}) if err != nil { return autogeneratedRoute{}, fmt.Errorf("failed to list alert rules: %w", err) @@ -60,11 +68,14 @@ func newAutogeneratedRoute[R receiver](ctx context.Context, logger log.Logger, s for _, setting := range ruleSettings { // TODO we should register this errors and somehow present to the users or make sure the config is always valid. if err = validator.Validate(setting); err != nil { - if skipInvalid { - logger.Error("Rule notification settings are invalid. Skipping", append(ruleKey.LogContext(), "error", err)...) - continue + switch invalidReceiverAction { + case ErrorOnInvalidReceivers: + return autogeneratedRoute{}, fmt.Errorf("invalid notification settings for rule %s: %w", ruleKey.UID, err) + case LogInvalidReceivers: + logger.Warn("Rule notification settings are invalid. Skipping", append(ruleKey.LogContext(), "error", err)...) + case IgnoreInvalidReceivers: // do nothing } - return autogeneratedRoute{}, fmt.Errorf("invalid notification settings for rule %s: %w", ruleKey.UID, err) + continue } fp := setting.Fingerprint(features) // Keep only unique settings. diff --git a/pkg/services/ngalert/notifier/autogen_alertmanager_test.go b/pkg/services/ngalert/notifier/autogen_alertmanager_test.go index bf376502caa..f0695e513bb 100644 --- a/pkg/services/ngalert/notifier/autogen_alertmanager_test.go +++ b/pkg/services/ngalert/notifier/autogen_alertmanager_test.go @@ -289,8 +289,11 @@ func TestAddAutogenConfig(t *testing.T) { for _, setting := range tt.storeSettings { store.notificationSettings[orgId][models.AlertRuleKey{OrgID: orgId, UID: util.GenerateShortUID()}] = []models.NotificationSettings{setting} } - - err := AddAutogenConfig(context.Background(), &logtest.Fake{}, store, orgId, tt.existingConfig, tt.skipInvalid, nil) + onInvalid := ErrorOnInvalidReceivers + if tt.skipInvalid { + onInvalid = IgnoreInvalidReceivers + } + err := AddAutogenConfig(context.Background(), &logtest.Fake{}, store, orgId, tt.existingConfig, onInvalid, nil) if tt.expErrorContains != "" { require.Error(t, err) require.ErrorContains(t, err, tt.expErrorContains) diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 85e7733f4bd..221f3e805d0 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -476,6 +476,12 @@ func TestReceiverService_Create(t *testing.T) { }, }, }, + { + name: "receiver with empty name fails", + user: writer, + receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithName("")), + expectedErr: legacy_storage.ErrReceiverInvalid, + }, } { t.Run(tc.name, func(t *testing.T) { sut := createReceiverServiceSut(t, &secretsService) @@ -901,6 +907,16 @@ func TestReceiverService_UpdateReceiverName(t *testing.T) { assert.NotNil(t, ruleStore.Calls[0].Args[4]) assert.Falsef(t, ruleStore.Calls[0].Args[5].(bool), "dryrun expected to be false") }) + + t.Run("returns ErrReceiverInvalid if empty name", func(t *testing.T) { + ruleStore := &fakeAlertRuleNotificationStore{} + sut := createReceiverServiceSut(t, &secretsService) + sut.ruleNotificationsStore = ruleStore + baseReceiver.Name = "" + + _, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer) + require.ErrorIs(t, err, legacy_storage.ErrReceiverInvalid) + }) } func TestReceiverServiceAC_Read(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go index d25aa4e959f..14abb8ceaa5 100644 --- a/pkg/services/ngalert/provisioning/contactpoints.go +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -309,13 +309,14 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in return err } - oldReceiverName, fullRemoval, newReceiverCreated := stitchReceiver(revision.Config, mergedReceiver) - if oldReceiverName == "" { - return fmt.Errorf("contact point with uid '%s' not found", mergedReceiver.UID) + oldReceiverNameRef, fullRemoval, newReceiverCreated := stitchReceiver(revision.Config, mergedReceiver) + if oldReceiverNameRef == nil { + return fmt.Errorf("%w: contact point with uid '%s' not found", ErrNotFound, mergedReceiver.UID) } + oldReceiverName := *oldReceiverNameRef err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error { - if mergedReceiver.Name != oldReceiverName { + if mergedReceiver.Name != oldReceiverName && oldReceiverName != "" { if newReceiverCreated { // Copy receiver permissions permissionsUpdated, err := ecp.resourcePermissions.CopyPermissions(ctx, orgID, nil, legacy_storage.NameToUid(oldReceiverName), legacy_storage.NameToUid(mergedReceiver.Name)) @@ -373,12 +374,12 @@ func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID in } } } - if fullRemoval && revision.ReceiverNameUsedByRoutes(name) { + if fullRemoval && name != "" && revision.ReceiverNameUsedByRoutes(name) { return ErrContactPointReferenced.Errorf("") } return ecp.xact.InTransaction(ctx, func(ctx context.Context) error { - if fullRemoval { + if fullRemoval && name != "" { used, err := ecp.notificationSettingsStore.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgID, ReceiverName: name}) if err != nil { return fmt.Errorf("failed to query alert rules for reference to the contact point '%s': %w", name, err) @@ -443,7 +444,7 @@ func (ecp *ContactPointService) encryptValue(value string) (string, error) { // stitchReceiver modifies a receiver, target, in an alertmanager configStore. It modifies the given configStore in-place. // Returns true if the configStore was altered in any way, and false otherwise. // If integration was moved to another group and it was the last in the previous group, the second parameter contains the name of the old group that is gone -func stitchReceiver(cfg *apimodels.PostableUserConfig, target *apimodels.PostableGrafanaReceiver) (oldReceiverName string, fullRemoval bool, newReceiverCreated bool) { +func stitchReceiver(cfg *apimodels.PostableUserConfig, target *apimodels.PostableGrafanaReceiver) (oldReceiverName *string, fullRemoval bool, newReceiverCreated bool) { // Algorithm to fix up receivers. Receivers are very complex and depend heavily on internal consistency. // All receivers in a given receiver group have the same name. We must maintain this across renames. groupLoop: @@ -451,7 +452,8 @@ groupLoop: // Does the current group contain the grafana receiver we're interested in? for i, grafanaReceiver := range receiverGroup.GrafanaManagedReceivers { if grafanaReceiver.UID == target.UID { - oldReceiverName = receiverGroup.Name + name := receiverGroup.Name + oldReceiverName = &name // If it's a basic field change, simply replace it. Done! // // NOTE: @@ -519,6 +521,9 @@ groupLoop: } func ValidateContactPoint(ctx context.Context, e *apimodels.EmbeddedContactPoint, decryptFunc alertingNotify.GetDecryptedValueFn) error { + if e.Name == "" { + return errors.New("name is required") + } iType, err := alertingNotify.IntegrationTypeFromString(e.Type) if err != nil { return err diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 34535c1a504..ddddadb1104 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -114,16 +114,6 @@ func TestIntegrationContactPointService(t *testing.T) { require.Equal(t, customUID, cps[0].UID) }) - t.Run("it's not possible to use invalid UID", func(t *testing.T) { - customUID := strings.Repeat("1", util.MaxUIDLength+1) - sut := createContactPointServiceSut(t, secretsService) - newCp := createTestContactPoint() - newCp.UID = customUID - - _, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI) - require.ErrorIs(t, err, ErrValidation) - }) - t.Run("it's not possible to use the same uid twice", func(t *testing.T) { customUID := "1337" sut := createContactPointServiceSut(t, secretsService) @@ -137,14 +127,40 @@ func TestIntegrationContactPointService(t *testing.T) { require.Error(t, err) }) - t.Run("create rejects contact points that fail validation", func(t *testing.T) { + t.Run("create rejects invalid contact points", func(t *testing.T) { sut := createContactPointServiceSut(t, secretsService) - newCp := createTestContactPoint() - newCp.Type = "" + testCases := []struct { + name string + cp func(*definitions.EmbeddedContactPoint) + }{ + { + name: "empty type", + cp: func(cp *definitions.EmbeddedContactPoint) { + cp.Type = "" + }, + }, + { + name: "empty name", + cp: func(cp *definitions.EmbeddedContactPoint) { + cp.Name = "" + }, + }, + { + name: "invalid UID", + cp: func(cp *definitions.EmbeddedContactPoint) { + cp.UID = strings.Repeat("1", util.MaxUIDLength+1) + }, + }, + } - _, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI) - - require.ErrorIs(t, err, ErrValidation) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + newCp := createTestContactPoint() + tc.cp(&newCp) + _, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI) + require.ErrorIs(t, err, ErrValidation) + }) + } }) t.Run("create accepts contact point with type in different cases", func(t *testing.T) { @@ -162,40 +178,57 @@ func TestIntegrationContactPointService(t *testing.T) { assert.EqualValues(t, slack.Type, got[0].Type) }) - t.Run("update rejects contact points with no settings", func(t *testing.T) { + t.Run("update rejects invalid contact points", func(t *testing.T) { + testCases := []struct { + name string + cp func(*definitions.EmbeddedContactPoint) + }{ + { + name: "empty type", + cp: func(cp *definitions.EmbeddedContactPoint) { + cp.Type = "" + }, + }, + { + name: "empty name", + cp: func(cp *definitions.EmbeddedContactPoint) { + cp.Name = "" + }, + }, + { + name: "nil settings", + cp: func(cp *definitions.EmbeddedContactPoint) { + cp.Settings = nil + }, + }, + { + name: "invalid settings after merge", + cp: func(cp *definitions.EmbeddedContactPoint) { + cp.Settings, _ = simplejson.NewJson([]byte(`{}`)) + }, + }, + } + sut := createContactPointServiceSut(t, secretsService) newCp := createTestContactPoint() newCp, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI) require.NoError(t, err) - newCp.Settings = nil - err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) - - require.ErrorIs(t, err, ErrValidation) - }) - - t.Run("update rejects contact points with no type", func(t *testing.T) { - sut := createContactPointServiceSut(t, secretsService) - newCp := createTestContactPoint() - newCp, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI) - require.NoError(t, err) - newCp.Type = "" - - err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) - - require.ErrorIs(t, err, ErrValidation) - }) - - t.Run("update rejects contact points which fail validation after merging", func(t *testing.T) { - sut := createContactPointServiceSut(t, secretsService) - newCp := createTestContactPoint() - newCp, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI) - require.NoError(t, err) - newCp.Settings, _ = simplejson.NewJson([]byte(`{}`)) - - err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) - - require.ErrorIs(t, err, ErrValidation) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cp := definitions.EmbeddedContactPoint{ + UID: newCp.UID, + Name: newCp.Name, + Type: newCp.Type, + Settings: newCp.Settings.DeepCopy(), + DisableResolveMessage: newCp.DisableResolveMessage, + Provenance: newCp.Provenance, + } + tc.cp(&cp) + err = sut.UpdateContactPoint(context.Background(), 1, cp, models.ProvenanceAPI) + require.ErrorIs(t, err, ErrValidation) + }) + } }) t.Run("update accepts contact points with type in another case", func(t *testing.T) { @@ -592,7 +625,7 @@ func TestStitchReceivers(t *testing.T) { initial *definitions.PostableUserConfig new *definitions.PostableGrafanaReceiver expCfg definitions.PostableApiAlertingConfig - expOldReceiver string + expOldReceiver *string expCreatedReceiver bool expFullRemoval bool } @@ -603,7 +636,7 @@ func TestStitchReceivers(t *testing.T) { new: &definitions.PostableGrafanaReceiver{ UID: "does not exist", }, - expOldReceiver: "", + expOldReceiver: nil, expCfg: createTestConfigWithReceivers().AlertmanagerConfig, }, { @@ -613,7 +646,7 @@ func TestStitchReceivers(t *testing.T) { Name: "receiver-2", Type: "teams", }, - expOldReceiver: "receiver-2", + expOldReceiver: util.Pointer("receiver-2"), expCfg: definitions.PostableApiAlertingConfig{ Config: definitions.Config{ Route: &definitions.Route{ @@ -674,7 +707,7 @@ func TestStitchReceivers(t *testing.T) { Name: "new-receiver", Type: "slack", }, - expOldReceiver: "receiver-1", + expOldReceiver: util.Pointer("receiver-1"), expCreatedReceiver: true, expFullRemoval: true, expCfg: definitions.PostableApiAlertingConfig{ @@ -737,7 +770,7 @@ func TestStitchReceivers(t *testing.T) { Name: "receiver-1", Type: "slack", }, - expOldReceiver: "receiver-2", + expOldReceiver: util.Pointer("receiver-2"), expCreatedReceiver: false, expCfg: definitions.PostableApiAlertingConfig{ Config: definitions.Config{ @@ -858,7 +891,7 @@ func TestStitchReceivers(t *testing.T) { Name: "receiver-2", Type: "slack", }, - expOldReceiver: "receiver-1", + expOldReceiver: util.Pointer("receiver-1"), expCreatedReceiver: false, expCfg: definitions.PostableApiAlertingConfig{ Config: definitions.Config{ @@ -1002,7 +1035,7 @@ func TestStitchReceivers(t *testing.T) { Name: "receiver-4", Type: "slack", }, - expOldReceiver: "receiver-1", + expOldReceiver: util.Pointer("receiver-1"), expCreatedReceiver: false, expCfg: definitions.PostableApiAlertingConfig{ Config: definitions.Config{ @@ -1087,7 +1120,7 @@ func TestStitchReceivers(t *testing.T) { Name: "brand-new-group", Type: "opsgenie", }, - expOldReceiver: "receiver-2", + expOldReceiver: util.Pointer("receiver-2"), expCreatedReceiver: true, expCfg: definitions.PostableApiAlertingConfig{ Config: definitions.Config{ @@ -1159,7 +1192,7 @@ func TestStitchReceivers(t *testing.T) { Name: "brand-new-group", Type: "opsgenie", }, - expOldReceiver: "receiver-2", // Not the inconsistent receiver-3? + expOldReceiver: util.Pointer("receiver-2"), // Not the inconsistent receiver-3? expCreatedReceiver: true, expCfg: definitions.PostableApiAlertingConfig{ Config: definitions.Config{ @@ -1282,7 +1315,7 @@ func TestStitchReceivers(t *testing.T) { Name: "receiver-1", Type: "slack", }, - expOldReceiver: "receiver-2", + expOldReceiver: util.Pointer("receiver-2"), expCreatedReceiver: false, expFullRemoval: true, expCfg: definitions.PostableApiAlertingConfig{ @@ -1337,7 +1370,7 @@ func TestStitchReceivers(t *testing.T) { } renamedReceiver, fullRemoval, createdReceiver := stitchReceiver(cfg, c.new) - assert.Equalf(t, c.expOldReceiver, renamedReceiver, "expected old receiver to be %s, got %s", c.expOldReceiver, renamedReceiver) + assert.EqualValuesf(t, c.expOldReceiver, renamedReceiver, "expected old receiver to be %v, got %v", c.expOldReceiver, renamedReceiver) assert.Equalf(t, c.expFullRemoval, fullRemoval, "expected full removal to be %t, got %t", c.expFullRemoval, fullRemoval) assert.Equalf(t, c.expCreatedReceiver, createdReceiver, "expected created receiver to be %t, got %t", c.expCreatedReceiver, createdReceiver) require.Equal(t, c.expCfg, cfg.AlertmanagerConfig) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 78fbbfe3a5b..81175e14766 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "hash/fnv" "maps" @@ -13,6 +14,7 @@ import ( "strings" "time" + openapiRuntime "github.com/go-openapi/runtime" "github.com/go-openapi/strfmt" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -47,10 +49,10 @@ type stateStore interface { } // AutogenFn is a function that adds auto-generated routes to a configuration. -type AutogenFn func(ctx context.Context, logger log.Logger, orgId int64, config *apimodels.PostableApiAlertingConfig, skipInvalid bool) error +type AutogenFn func(ctx context.Context, logger log.Logger, orgId int64, config *apimodels.PostableApiAlertingConfig, invalidReceiverAction notifier.InvalidReceiversAction) error // NoopAutogenFn is used to skip auto-generating routes. -func NoopAutogenFn(_ context.Context, _ log.Logger, _ int64, _ *apimodels.PostableApiAlertingConfig, _ bool) error { +func NoopAutogenFn(_ context.Context, _ log.Logger, _ int64, _ *apimodels.PostableApiAlertingConfig, _ notifier.InvalidReceiversAction) error { return nil } @@ -206,7 +208,7 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto // (grouping, group timing, time intervals etc) changes the autogenerated configuration. // The `default` flag is sent to the remote Alertmanager for informational purposes, so we can tolerate this. err = func() error { - defaultCfg, err := am.buildConfiguration(ctx, []byte(cfg.DefaultConfig), 0) + defaultCfg, err := am.buildConfiguration(ctx, []byte(cfg.DefaultConfig), 0, notifier.IgnoreInvalidReceivers) if err != nil { return fmt.Errorf("unable to build default configuration: %w", err) } @@ -271,7 +273,7 @@ func (am *Alertmanager) checkReadiness(ctx context.Context) error { // CompareAndSendConfiguration checks whether a given configuration is being used by the remote Alertmanager. // If not, it sends the configuration to the remote Alertmanager. func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config *models.AlertConfiguration) error { - payload, err := am.buildConfiguration(ctx, []byte(config.AlertmanagerConfiguration), config.CreatedAt) + payload, err := am.buildConfiguration(ctx, []byte(config.AlertmanagerConfiguration), config.CreatedAt, notifier.LogInvalidReceivers) if err != nil { return fmt.Errorf("unable to build configuration: %w", err) } @@ -303,14 +305,14 @@ func decrypter(ctx context.Context, crypto Crypto) models.DecryptFn { // buildConfiguration takes a raw Alertmanager configuration and returns a config that the remote Alertmanager can use. // It parses the initial configuration, adds auto-generated routes, decrypts receivers, and merges the extra configs. -func (am *Alertmanager) buildConfiguration(ctx context.Context, raw []byte, createdAtEpoch int64) (remoteClient.UserGrafanaConfig, error) { +func (am *Alertmanager) buildConfiguration(ctx context.Context, raw []byte, createdAtEpoch int64, autogenInvalidReceiverAction notifier.InvalidReceiversAction) (remoteClient.UserGrafanaConfig, error) { c, err := notifier.Load(raw) if err != nil { return remoteClient.UserGrafanaConfig{}, err } // Add auto-generated routes and decrypt before comparing. - if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, true); err != nil { + if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, autogenInvalidReceiverAction); err != nil { return remoteClient.UserGrafanaConfig{}, err } @@ -433,7 +435,7 @@ func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P return err } - payload, err := am.buildConfiguration(ctx, rawCopy, time.Now().Unix()) + payload, err := am.buildConfiguration(ctx, rawCopy, time.Now().Unix(), notifier.LogInvalidReceivers) if err != nil { return fmt.Errorf("unable to build configuration: %w", err) } @@ -444,7 +446,7 @@ func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P // SaveAndApplyDefaultConfig sends the default Grafana Alertmanager configuration to the remote Alertmanager. func (am *Alertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error { am.log.Debug("Sending default configuration to a remote Alertmanager", "url", am.url) - payload, err := am.buildConfiguration(ctx, []byte(am.defaultConfig), time.Now().Unix()) + payload, err := am.buildConfiguration(ctx, []byte(am.defaultConfig), time.Now().Unix(), notifier.LogInvalidReceivers) if err != nil { return fmt.Errorf("unable to build default configuration: %w", err) } @@ -465,6 +467,18 @@ func (am *Alertmanager) CreateSilence(ctx context.Context, silence *apimodels.Po params := amsilence.NewPostSilencesParamsWithContext(ctx).WithSilence(silence) res, err := am.amClient.Silence.PostSilences(params) if err != nil { + // Translate downstream 4xx errors into a well-known bad payload error so callers can surface HTTP 400. + // The swagger client returns typed errors and/or an *openapiRuntime.APIError with a Code field. + var badReq *amsilence.PostSilencesBadRequest + if errors.As(err, &badReq) { + return "", fmt.Errorf("%w: %v", alertingNotify.ErrCreateSilenceBadPayload, err) + } + var apiErr *openapiRuntime.APIError + if errors.As(err, &apiErr) { + if apiErr.Code >= http.StatusBadRequest && apiErr.Code < http.StatusInternalServerError { + return "", fmt.Errorf("%w: %v", alertingNotify.ErrCreateSilenceBadPayload, err) + } + } return "", err } diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 0ca0bbc950f..b183943d2ac 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -453,7 +453,7 @@ func TestCompareAndSendConfiguration(t *testing.T) { DefaultConfig: defaultGrafanaConfig, } - testAutogenFn := func(_ context.Context, _ log.Logger, _ int64, config *apimodels.PostableApiAlertingConfig, _ bool) error { + testAutogenFn := func(_ context.Context, _ log.Logger, _ int64, config *apimodels.PostableApiAlertingConfig, _ notifier.InvalidReceiversAction) error { newRoute := definition.Route{ Receiver: config.Receivers[0].Name, Match: map[string]string{"auto-gen-test": "true"}, @@ -497,7 +497,7 @@ func TestCompareAndSendConfiguration(t *testing.T) { testAutogenRoutes, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) require.NoError(t, err) - require.NoError(t, testAutogenFn(nil, nil, 0, &testAutogenRoutes.AlertmanagerConfig, false)) + require.NoError(t, testAutogenFn(nil, nil, 0, &testAutogenRoutes.AlertmanagerConfig, notifier.ErrorOnInvalidReceivers)) cfgWithAutogenRoutes := client.GrafanaAlertmanagerConfig{ TemplateFiles: testAutogenRoutes.TemplateFiles, AlertmanagerConfig: testAutogenRoutes.AlertmanagerConfig, @@ -1420,7 +1420,7 @@ func genAlert(active bool, labels map[string]string) amv2.PostableAlert { } // errAutogenFn is an AutogenFn that always returns an error. -func errAutogenFn(_ context.Context, _ log.Logger, _ int64, _ *definition.PostableApiAlertingConfig, _ bool) error { +func errAutogenFn(_ context.Context, _ log.Logger, _ int64, _ *definition.PostableApiAlertingConfig, _ notifier.InvalidReceiversAction) error { return errTest } diff --git a/pkg/services/ngalert/state/persist.go b/pkg/services/ngalert/state/persist.go index 7c7442b986e..1ad5beed0ca 100644 --- a/pkg/services/ngalert/state/persist.go +++ b/pkg/services/ngalert/state/persist.go @@ -8,6 +8,10 @@ import ( history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) +type AlertInstancesProvider interface { + GetAlertInstances() []models.AlertInstance +} + // InstanceStore represents the ability to fetch and write alert instances. type InstanceStore interface { InstanceReader diff --git a/pkg/services/ngalert/state/persister_async.go b/pkg/services/ngalert/state/persister_async.go index 0de1dd5cdde..f76eed5f06e 100644 --- a/pkg/services/ngalert/state/persister_async.go +++ b/pkg/services/ngalert/state/persister_async.go @@ -12,10 +12,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ) -type AlertInstancesProvider interface { - GetAlertInstances() []models.AlertInstance -} - type AsyncStatePersister struct { log log.Logger batchSize int diff --git a/pkg/services/ngalert/state/persister_sync_rule.go b/pkg/services/ngalert/state/persister_sync_rule.go index da2494d3647..a237d2983f2 100644 --- a/pkg/services/ngalert/state/persister_sync_rule.go +++ b/pkg/services/ngalert/state/persister_sync_rule.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/benbjohnson/clock" "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/infra/log" @@ -11,22 +12,63 @@ import ( ) type SyncRuleStatePersister struct { - log log.Logger - store InstanceStore + log log.Logger + store InstanceStore + ticker *clock.Ticker } -func NewSyncRuleStatePersisiter(log log.Logger, cfg ManagerCfg) StatePersister { +func NewSyncRuleStatePersisiter(log log.Logger, ticker *clock.Ticker, cfg ManagerCfg) StatePersister { return &SyncRuleStatePersister{ - log: log, - store: cfg.InstanceStore, + log: log, + store: cfg.InstanceStore, + ticker: ticker, } } -func (a *SyncRuleStatePersister) Async(_ context.Context, _ AlertInstancesProvider) { - a.log.Debug("Async: No-Op") +func (a *SyncRuleStatePersister) Async(ctx context.Context, instancesProvider AlertInstancesProvider) { + if a.ticker == nil { + return + } + + for { + select { + case <-a.ticker.C: + if err := a.fullSync(ctx, instancesProvider); err != nil { + a.log.Error("Failed to do a full compressed state sync to database", "err", err) + } + case <-ctx.Done(): + a.log.Info("Scheduler is shutting down, doing a final state sync.") + if err := a.fullSync(context.Background(), instancesProvider); err != nil { + a.log.Error("Failed to do a full compressed state sync to database", "err", err) + } + a.ticker.Stop() + a.log.Info("Compressed state async worker is shut down.") + return + } + } +} + +func (a *SyncRuleStatePersister) fullSync(ctx context.Context, instancesProvider AlertInstancesProvider) error { + startTime := time.Now() + a.log.Debug("Full compressed state sync start") + instances := instancesProvider.GetAlertInstances() + + // batchSize is set to 0 because compressed storage groups instances by ruleUID, not by batch size + err := a.store.FullSync(ctx, instances, 0, nil) + if err != nil { + a.log.Error("Full compressed state sync failed", "duration", time.Since(startTime), "instances", len(instances)) + return err + } + a.log.Debug("Full compressed state sync done", "duration", time.Since(startTime), "instances", len(instances)) + return nil } func (a *SyncRuleStatePersister) Sync(ctx context.Context, span trace.Span, ruleKey models.AlertRuleKeyWithGroup, states StateTransitions) { + if a.ticker != nil { + a.log.Debug("Skip immediate save, using periodic save instead") + return + } + if a.store == nil || len(states) == 0 { return } diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 90d557abf0d..d1ac3f59cd4 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -1460,6 +1460,148 @@ func TestIntegrationRuleGroupsCaseSensitive(t *testing.T) { }) } +// To address issues arising from case-insensitive collations in some databases (e.g., MySQL/MariaDB), +func TestIntegrationListAlertRulesByGroupCaseSensitiveOrdering(t *testing.T) { + tutil.SkipIntegrationTestInShortMode(t) + + usr := models.UserUID("test") + + sqlStore := db.InitTestDB(t) + cfg := setting.NewCfg() + cfg.UnifiedAlerting.BaseInterval = 1 * time.Second + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + logger := log.New("test-dbstore") + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + store.FeatureToggles = featuremgmt.WithFeatures() + + gen := models.RuleGen.With(models.RuleMuts.WithOrgID(1)) + + // Create namespace and base group key + groupKey := models.GenerateGroupKey(1) + + // Create groups with case-sensitive names: "TEST", "Test", "test" + groupKeyUpper := groupKey + groupKeyUpper.RuleGroup = "TEST" + + groupKeyMixed := groupKey + groupKeyMixed.RuleGroup = "Test" + + groupKeyLower := groupKey + groupKeyLower.RuleGroup = "test" + + // Generate rules for each group + groupUpper := gen.With(gen.WithGroupKey(groupKeyUpper)).GenerateMany(2) + groupMixed := gen.With(gen.WithGroupKey(groupKeyMixed)).GenerateMany(2) + groupLower := gen.With(gen.WithGroupKey(groupKeyLower)).GenerateMany(2) + + // Insert all rules + allRules := append(append(groupUpper, groupMixed...), groupLower...) + _, err := store.InsertAlertRules(context.Background(), &usr, allRules) + require.NoError(t, err) + + t.Run("should order groups case-sensitively", func(t *testing.T) { + result, _, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ + ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: 1}, + }) + require.NoError(t, err) + require.Len(t, result, 6, "should return all 6 rules") + + // Extract group names in order + var groupOrder []string + for _, rule := range result { + if len(groupOrder) == 0 || groupOrder[len(groupOrder)-1] != rule.RuleGroup { + groupOrder = append(groupOrder, rule.RuleGroup) + } + } + + // Verify case-sensitive alphabetical ordering + // different databases may sort uppercase before lowercase or vice versa depending on character set, the important part is that the order is consistent and case-sensitive + expectedOrder := []string{"test", "Test", "TEST"} + alternateExpectedOrder := []string{"TEST", "Test", "test"} + if !slices.Equal(groupOrder, expectedOrder) && !slices.Equal(groupOrder, alternateExpectedOrder) { + t.Fatalf("groups are not ordered case-sensitively as expected. got: %v, want: %v or %v", groupOrder, expectedOrder, alternateExpectedOrder) + } + + // Verify each group contains the correct rules + groupRules := make(map[string][]*models.AlertRule) + for _, rule := range result { + groupRules[rule.RuleGroup] = append(groupRules[rule.RuleGroup], rule) + } + + require.Len(t, groupRules["TEST"], 2, "TEST group should have 2 rules") + require.Len(t, groupRules["Test"], 2, "Test group should have 2 rules") + require.Len(t, groupRules["test"], 2, "test group should have 2 rules") + }) + + t.Run("should respect group limit with case-sensitive ordering", func(t *testing.T) { + // Test with limit of 2 groups - should get first 2 groups in case-sensitive order + result, continueToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ + ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: 1}, + Limit: 2, + }) + require.NoError(t, err) + require.Len(t, result, 4, "should return 4 rules (2 rules from first 2 groups)") + require.NotEmpty(t, continueToken, "should have continue token when limit is reached") + + // Extract group names from limited result + var limitedGroupOrder []string + for _, rule := range result { + if len(limitedGroupOrder) == 0 || limitedGroupOrder[len(limitedGroupOrder)-1] != rule.RuleGroup { + limitedGroupOrder = append(limitedGroupOrder, rule.RuleGroup) + } + } + + // Should get first 2 groups in case-sensitive order: "TEST", "Test" or "test", "Test" + expectedLimitedOrder := []string{"TEST", "Test"} + alternateExpectedOrder := []string{"test", "Test"} + matchesDescLexOrder := slices.Equal(limitedGroupOrder, expectedLimitedOrder) + matchesAscLexOrder := slices.Equal(limitedGroupOrder, alternateExpectedOrder) + if !matchesDescLexOrder && !matchesAscLexOrder { + t.Fatalf("limited groups are not ordered case-sensitively as expected. got: %v, want: %v or %v", limitedGroupOrder, expectedLimitedOrder, alternateExpectedOrder) + } + + // Continue from token to get remaining groups + remainingResult, nextToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ + ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: 1}, + ContinueToken: continueToken, + }) + require.NoError(t, err) + require.Len(t, remainingResult, 2, "should return 2 rules from remaining group") + require.Empty(t, nextToken, "should not have continue token when all groups are fetched") + + lastGroup := "test" + if matchesAscLexOrder { + lastGroup = "TEST" + } + + // Verify the remaining group is "test" + for _, rule := range remainingResult { + require.Equal(t, lastGroup, rule.RuleGroup, "remaining group should be 'test'") + } + }) + + t.Run("should handle group limit of 1 correctly", func(t *testing.T) { + result, continueToken, err := store.ListAlertRulesByGroup(context.Background(), &models.ListAlertRulesExtendedQuery{ + ListAlertRulesQuery: models.ListAlertRulesQuery{OrgID: 1}, + Limit: 1, + }) + require.NoError(t, err) + require.Len(t, result, 2, "should return 2 rules from first group") + require.NotEmpty(t, continueToken, "should have continue token") + + // Should only get the first group which can be "TEST" or "test" depending on charset + expectedGroup := "TEST" + if result[0].RuleGroup == "test" { + expectedGroup = "test" + } + + for _, rule := range result { + require.Equal(t, expectedGroup, rule.RuleGroup, "all rules should be from the first group") + } + }) +} + func TestIntegrationIncreaseVersionForAllRulesInNamespaces(t *testing.T) { tutil.SkipIntegrationTestInShortMode(t) diff --git a/pkg/services/ngalert/store/proto_instance_database.go b/pkg/services/ngalert/store/proto_instance_database.go index eaa58253ac4..a9450d0851f 100644 --- a/pkg/services/ngalert/store/proto_instance_database.go +++ b/pkg/services/ngalert/store/proto_instance_database.go @@ -9,6 +9,7 @@ import ( "time" "github.com/golang/snappy" + "github.com/grafana/grafana/pkg/services/sqlstore" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -98,28 +99,13 @@ func (st ProtoInstanceDBStore) SaveAlertInstancesForRule(ctx context.Context, ke logger := st.Logger.FromContext(ctx) logger.Debug("SaveAlertInstancesForRule called", "rule_uid", key.UID, "org_id", key.OrgID, "instances", len(instances)) - alert_instances_proto := make([]*pb.AlertInstance, len(instances)) - - for i, instance := range instances { - alert_instances_proto[i] = alertInstanceModelToProto(instance) - } - - compressedAlertInstances, err := compressAlertInstances(alert_instances_proto) + compressedAlertInstances, err := convertAndCompressAlertInstances(instances) if err != nil { return fmt.Errorf("failed to compress alert instances: %w", err) } - return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - params := []any{key.OrgID, key.UID, compressedAlertInstances, time.Now()} - - upsertSQL := st.SQLStore.GetDialect().UpsertSQL( - "alert_rule_state", - []string{"org_id", "rule_uid"}, - []string{"org_id", "rule_uid", "data", "updated_at"}, - ) - _, err = sess.SQL(upsertSQL, params...).Query() - - return err + return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + return st.upsertCompressedAlertInstances(sess, key.OrgID, key.UID, compressedAlertInstances, time.Now()) }) } @@ -134,9 +120,68 @@ func (st ProtoInstanceDBStore) DeleteAlertInstancesByRule(ctx context.Context, k } func (st ProtoInstanceDBStore) FullSync(ctx context.Context, instances []models.AlertInstance, batchSize int, jitterFunc func(int) time.Duration) error { + if len(instances) == 0 { + return nil + } + logger := st.Logger.FromContext(ctx) - logger.Error("FullSync called and not implemented") - return errors.New("fullsync is not implemented for proto instance database store") + logger.Debug("FullSync called", "total_instances", len(instances)) + + ruleGroups := make(map[models.AlertRuleKeyWithGroup][]models.AlertInstance) + for _, instance := range instances { + ruleKey := models.AlertRuleKeyWithGroup{ + AlertRuleKey: models.AlertRuleKey{ + OrgID: instance.RuleOrgID, + UID: instance.RuleUID, + }, + RuleGroup: "", + } + ruleGroups[ruleKey] = append(ruleGroups[ruleKey], instance) + } + + type preparedRule struct { + ruleKey models.AlertRuleKeyWithGroup + compressedData []byte + } + preparedRules := make([]preparedRule, 0, len(ruleGroups)) + + for ruleKey, ruleInstances := range ruleGroups { + // Convert and compress instances + compressedAlertInstances, err := convertAndCompressAlertInstances(ruleInstances) + if err != nil { + logger.Error("Failed to compress instances for rule", "rule_uid", ruleKey.UID, "error", err) + continue + } + + preparedRules = append(preparedRules, preparedRule{ + ruleKey: ruleKey, + compressedData: compressedAlertInstances, + }) + + logger.Debug("Prepared rule for sync", "rule_uid", ruleKey.UID, "org_id", ruleKey.OrgID, "instances", len(ruleInstances)) + } + + return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + syncTimestamp := time.Now() + logger.Debug("Starting FullSync transaction", "rules_count", len(preparedRules), "timestamp", syncTimestamp) + + // First we delete all records from the table + if _, err := sess.Exec("DELETE FROM alert_rule_state"); err != nil { + return fmt.Errorf("failed to delete alert_rule_state: %w", err) + } + + for i, prepared := range preparedRules { + logger.Debug("Executing UPSERT for rule", "rule_uid", prepared.ruleKey.UID, "org_id", prepared.ruleKey.OrgID, "rule_index", i+1, "total_rules", len(preparedRules)) + + // Execute UPSERT with pre-compressed data using helper method + if err := st.upsertCompressedAlertInstances(sess, prepared.ruleKey.OrgID, prepared.ruleKey.UID, prepared.compressedData, syncTimestamp); err != nil { + return fmt.Errorf("failed to save instances for rule %s: %w", prepared.ruleKey.UID, err) + } + } + + logger.Debug("FullSync transaction completed successfully", "rules_synced", len(preparedRules)) + return nil + }) } func alertInstanceModelToProto(modelInstance models.AlertInstance) *pb.AlertInstance { @@ -155,6 +200,30 @@ func alertInstanceModelToProto(modelInstance models.AlertInstance) *pb.AlertInst } } +// convertAndCompressAlertInstances converts model instances to protobuf and compresses them +func convertAndCompressAlertInstances(instances []models.AlertInstance) ([]byte, error) { + alertInstancesProto := make([]*pb.AlertInstance, len(instances)) + for i, instance := range instances { + alertInstancesProto[i] = alertInstanceModelToProto(instance) + } + + return compressAlertInstances(alertInstancesProto) +} + +// upsertCompressedAlertInstances performs upsert operation for compressed alert instances +func (st ProtoInstanceDBStore) upsertCompressedAlertInstances(sess *sqlstore.DBSession, orgID int64, ruleUID string, compressedData []byte, timestamp time.Time) error { + upsertSQL := st.SQLStore.GetDialect().UpsertSQL( + "alert_rule_state", + []string{"org_id", "rule_uid"}, + []string{"org_id", "rule_uid", "data", "updated_at"}, + ) + + params := []any{orgID, ruleUID, compressedData, timestamp} + _, err := sess.SQL(upsertSQL, params...).Query() + + return err +} + func compressAlertInstances(instances []*pb.AlertInstance) ([]byte, error) { mProto, err := proto.Marshal(&pb.AlertInstances{Instances: instances}) if err != nil { diff --git a/pkg/services/ngalert/store/proto_instance_database_test.go b/pkg/services/ngalert/store/proto_instance_database_test.go index 04428a0efae..c98923995dd 100644 --- a/pkg/services/ngalert/store/proto_instance_database_test.go +++ b/pkg/services/ngalert/store/proto_instance_database_test.go @@ -174,6 +174,166 @@ func TestCompressAndDecompressAlertInstances(t *testing.T) { require.EqualExportedValues(t, alertInstances[1], decompressedInstances[1]) } +func TestConvertAndCompressAlertInstances(t *testing.T) { + now := time.Now() + + modelInstances := []models.AlertInstance{ + { + AlertInstanceKey: models.AlertInstanceKey{ + RuleUID: "rule-uid-1", + RuleOrgID: 1, + LabelsHash: "hash-1", + }, + Labels: map[string]string{"label-1": "value-1"}, + CurrentState: models.InstanceStateFiring, + CurrentStateSince: now, + CurrentStateEnd: now.Add(time.Hour), + CurrentReason: "reason-1", + LastEvalTime: now.Add(-time.Minute), + LastSentAt: &now, + FiredAt: &now, + ResolvedAt: nil, + ResultFingerprint: "fingerprint-1", + }, + { + AlertInstanceKey: models.AlertInstanceKey{ + RuleUID: "rule-uid-1", + RuleOrgID: 1, + LabelsHash: "hash-2", + }, + Labels: map[string]string{"label-2": "value-2"}, + CurrentState: models.InstanceStateNormal, + CurrentStateSince: now, + CurrentStateEnd: now.Add(time.Hour), + CurrentReason: "reason-2", + LastEvalTime: now.Add(-time.Minute), + LastSentAt: nil, + FiredAt: nil, + ResolvedAt: &now, + ResultFingerprint: "fingerprint-2", + }, + } + + compressedData, err := convertAndCompressAlertInstances(modelInstances) + require.NoError(t, err) + require.NotEmpty(t, compressedData) + + // Verify we can decompress and get back the same data + decompressedInstances, err := decompressAlertInstances(compressedData) + require.NoError(t, err) + require.Len(t, decompressedInstances, 2) + + // Convert back to model to compare + for i, protoInstance := range decompressedInstances { + modelInstance := alertInstanceProtoToModel("rule-uid-1", 1, protoInstance) + require.Equal(t, modelInstances[i].Labels, modelInstance.Labels) + require.Equal(t, modelInstances[i].CurrentState, modelInstance.CurrentState) + require.Equal(t, modelInstances[i].LabelsHash, modelInstance.LabelsHash) + require.Equal(t, modelInstances[i].ResultFingerprint, modelInstance.ResultFingerprint) + } +} + +func TestConvertAndCompressAlertInstances_EmptyInput(t *testing.T) { + emptyInstances := []models.AlertInstance{} + + compressedData, err := convertAndCompressAlertInstances(emptyInstances) + require.NoError(t, err) + + decompressedInstances, err := decompressAlertInstances(compressedData) + require.NoError(t, err) + require.Empty(t, decompressedInstances) +} + +func TestFullSyncGroupingLogic(t *testing.T) { + now := time.Now() + + // Test instances from multiple rules to verify grouping logic + instances := []models.AlertInstance{ + { + AlertInstanceKey: models.AlertInstanceKey{ + RuleUID: "rule-1", + RuleOrgID: 1, + LabelsHash: "hash-1-1", + }, + Labels: models.InstanceLabels{"rule1": "instance1"}, + CurrentState: models.InstanceStateFiring, + CurrentStateSince: now, + CurrentStateEnd: now.Add(time.Hour), + CurrentReason: "test reason 1", + LastEvalTime: now.Add(-time.Minute), + ResultFingerprint: "fingerprint-1-1", + }, + { + AlertInstanceKey: models.AlertInstanceKey{ + RuleUID: "rule-1", + RuleOrgID: 1, + LabelsHash: "hash-1-2", + }, + Labels: models.InstanceLabels{"rule1": "instance2"}, + CurrentState: models.InstanceStateNormal, + CurrentStateSince: now, + CurrentStateEnd: now.Add(time.Hour), + CurrentReason: "test reason 2", + LastEvalTime: now.Add(-time.Minute), + ResultFingerprint: "fingerprint-1-2", + }, + { + AlertInstanceKey: models.AlertInstanceKey{ + RuleUID: "rule-2", + RuleOrgID: 1, + LabelsHash: "hash-2-1", + }, + Labels: models.InstanceLabels{"rule2": "instance1"}, + CurrentState: models.InstanceStatePending, + CurrentStateSince: now, + CurrentStateEnd: now.Add(time.Hour), + CurrentReason: "test reason 3", + LastEvalTime: now.Add(-time.Minute), + ResultFingerprint: "fingerprint-2-1", + }, + } + + // Test the grouping logic that FullSync uses internally + ruleGroups := make(map[models.AlertRuleKeyWithGroup][]models.AlertInstance) + for _, instance := range instances { + ruleKey := models.AlertRuleKeyWithGroup{ + AlertRuleKey: models.AlertRuleKey{ + OrgID: instance.RuleOrgID, + UID: instance.RuleUID, + }, + RuleGroup: "", + } + ruleGroups[ruleKey] = append(ruleGroups[ruleKey], instance) + } + + // Verify grouping worked correctly + require.Len(t, ruleGroups, 2, "Should have 2 rule groups") + + rule1Key := models.AlertRuleKeyWithGroup{ + AlertRuleKey: models.AlertRuleKey{OrgID: 1, UID: "rule-1"}, + RuleGroup: "", + } + rule2Key := models.AlertRuleKeyWithGroup{ + AlertRuleKey: models.AlertRuleKey{OrgID: 1, UID: "rule-2"}, + RuleGroup: "", + } + + require.Len(t, ruleGroups[rule1Key], 2, "Rule 1 should have 2 instances") + require.Len(t, ruleGroups[rule2Key], 1, "Rule 2 should have 1 instance") + + // Test compression for each group + for ruleKey, ruleInstances := range ruleGroups { + compressedData, err := convertAndCompressAlertInstances(ruleInstances) + require.NoError(t, err, "Compression should succeed for rule %s", ruleKey.UID) + require.NotEmpty(t, compressedData, "Compressed data should not be empty for rule %s", ruleKey.UID) + + // Verify decompression works + decompressedInstances, err := decompressAlertInstances(compressedData) + require.NoError(t, err, "Decompression should succeed for rule %s", ruleKey.UID) + require.Len(t, decompressedInstances, len(ruleInstances), "Should have same number of instances after decompression for rule %s", ruleKey.UID) + } +} + func toProtoTimestampPtr(tm *time.Time) *timestamppb.Timestamp { if tm == nil { return nil diff --git a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go index 2ef0b24c162..14471178bd9 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go @@ -2,222 +2,56 @@ package clientmiddleware import ( "context" - "fmt" - "strconv" - "time" - "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/prometheus/client_golang/prometheus" - "golang.org/x/sync/singleflight" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/contexthandler" - "github.com/grafana/grafana/pkg/services/featuremgmt" ) -// needed to mock the function for testing -var shouldCacheQuery = awsds.ShouldCacheQuery - // NewCachingMiddleware creates a new backend.HandlerMiddleware that will // attempt to read and write query results to the cache -func NewCachingMiddleware(cachingService caching.CachingService) backend.HandlerMiddleware { - return NewCachingMiddlewareWithFeatureManager(cachingService, nil) -} - -// 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) backend.HandlerMiddleware { - log := log.New("caching_middleware") - if err := prometheus.Register(QueryCachingRequestHistogram); err != nil { - log.Error("Error registering prometheus collector 'QueryRequestHistogram'", "error", err) - } - if err := prometheus.Register(ResourceCachingRequestHistogram); err != nil { - log.Error("Error registering prometheus collector 'ResourceRequestHistogram'", "error", err) - } +func NewCachingMiddleware(cachingServiceClient *caching.CachingServiceClient) backend.HandlerMiddleware { cachingMiddlewareHandler := func(next backend.Handler) backend.Handler { - cachingMiddleware := &CachingMiddleware{ - BaseHandler: backend.NewBaseHandler(next), - caching: cachingService, - log: log, - features: features, + return &CachingMiddleware{ + BaseHandler: backend.NewBaseHandler(next), + cachingServiceClient: cachingServiceClient, } - if features != nil && features.IsEnabled(context.Background(), featuremgmt.FlagQueryCacheRequestDeduplication) { - return newRequestDeduplicationMiddleware(log, cachingMiddleware) - } - return cachingMiddleware } return backend.HandlerMiddlewareFunc(cachingMiddlewareHandler) } +// An adapter to use CachingServiceClient as a middleware. If possible prefer to use `CachingServiceClient` directly. type CachingMiddleware struct { backend.BaseHandler - caching caching.CachingService - log log.Logger - features featuremgmt.FeatureToggles + cachingServiceClient *caching.CachingServiceClient } // QueryData receives a data request and attempts to access results already stored in the cache for that request. // If data is found, it will return it immediately. Otherwise, it will perform the queries as usual, then write the response to the cache. // 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.BaseHandler.QueryData(ctx, req) - } - reqCtx := contexthandler.FromContext(ctx) if reqCtx == nil { return m.BaseHandler.QueryData(ctx, req) } - - // time how long this request takes - start := time.Now() - - // First look in the query cache if enabled - hit, cr := m.caching.HandleQueryRequest(ctx, req) - - // record request duration if caching was used - ch := reqCtx.Resp.Header().Get(caching.XCacheHeader) - if ch != "" { - defer func() { - QueryCachingRequestHistogram.With(prometheus.Labels{ - "datasource_type": req.PluginContext.DataSourceInstanceSettings.Type, - "cache": ch, - "query_type": getQueryType(reqCtx), - }).Observe(time.Since(start).Seconds()) - }() - } - - // Cache hit; return the response - if hit { - return cr.Response, nil - } - - // Cache miss; do the actual queries - resp, err := m.BaseHandler.QueryData(ctx, req) - - // Update the query cache with the result for this metrics request - if err == nil && cr.UpdateCacheFn != nil { - // If AWS async caching is not enabled, use the old code path - if m.features == nil || !m.features.IsEnabled(ctx, featuremgmt.FlagAwsAsyncQueryCaching) { - cr.UpdateCacheFn(ctx, resp) - } else { - // time how long shouldCacheQuery takes - startShouldCacheQuery := time.Now() - shouldCache := shouldCacheQuery(resp) - ShouldCacheQueryHistogram.With(prometheus.Labels{ - "datasource_type": req.PluginContext.DataSourceInstanceSettings.Type, - "cache": ch, - "shouldCache": strconv.FormatBool(shouldCache), - "query_type": getQueryType(reqCtx), - }).Observe(time.Since(startShouldCacheQuery).Seconds()) - - // If AWS async caching is enabled and resp is for a running async query, don't cache it - if shouldCache { - cr.UpdateCacheFn(ctx, resp) - } - } - } - - return resp, err + return m.cachingServiceClient.WithQueryDataCaching(ctx, req, func() (*backend.QueryDataResponse, error) { + return m.BaseHandler.QueryData(ctx, req) + }) } // CallResource receives a resource request and attempts to access results already stored in the cache for that request. // If data is found, it will return it immediately. Otherwise, it will perform the request as usual. The caller of CallResource is expected to explicitly update the cache with any responses. // 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.BaseHandler.CallResource(ctx, req, sender) - } - reqCtx := contexthandler.FromContext(ctx) if reqCtx == nil { return m.BaseHandler.CallResource(ctx, req, sender) } - // time how long this request takes - start := time.Now() - - // First look in the resource cache if enabled - hit, cr := m.caching.HandleResourceRequest(ctx, req) - - // record request duration if caching was used - if ch := reqCtx.Resp.Header().Get(caching.XCacheHeader); ch != "" { - defer func() { - ResourceCachingRequestHistogram.With(prometheus.Labels{ - "plugin_id": req.PluginContext.PluginID, - "cache": ch, - }).Observe(time.Since(start).Seconds()) - }() - } - - // Cache hit; send the response and return - if hit { - return sender.Send(cr.Response) - } - - // 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.cachingServiceClient.WithCallResourceCaching(ctx, req, sender, func(sender backend.CallResourceResponseSender) error { 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 { - cr.UpdateCacheFn(ctx, res) - return sender.Send(res) }) - - return m.BaseHandler.CallResource(ctx, req, cacheSender) -} - -// Given N requests happening at the same time and issuing the same query, only one request will execute -// and the other ones will wait for the response received by the request being executed. -type requestDeduplicationMiddleware struct { - backend.BaseHandler - log *log.ConcreteLogger - singleflight *singleflight.Group -} - -func newRequestDeduplicationMiddleware(log *log.ConcreteLogger, next backend.Handler) *requestDeduplicationMiddleware { - return &requestDeduplicationMiddleware{log: log, BaseHandler: backend.NewBaseHandler(next), singleflight: &singleflight.Group{}} -} - -func (m *requestDeduplicationMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if req.PluginContext.DataSourceInstanceSettings == nil || req.PluginContext.DataSourceInstanceSettings.UID == "" { - return m.BaseHandler.QueryData(ctx, req) - } - key, err := caching.GetKey(req.PluginContext.DataSourceInstanceSettings.UID, req) - if err != nil { - m.log.Error("error building cache key for request deduplication, skipping request deduplication", "error", err) - return m.BaseHandler.QueryData(ctx, req) - } - v, err, _ := m.singleflight.Do(key, func() (interface{}, error) { - return m.BaseHandler.QueryData(ctx, req) - }) - if err != nil { - return nil, fmt.Errorf("request deduplication middleware: calling BaseHandler.QueryData: %w", err) - } - return v.(*backend.QueryDataResponse), nil -} - -func (m *requestDeduplicationMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req.PluginContext.DataSourceInstanceSettings == nil || req.PluginContext.DataSourceInstanceSettings.UID == "" { - return m.BaseHandler.CallResource(ctx, req, sender) - } - - key, err := caching.GetKey(req.PluginContext.DataSourceInstanceSettings.UID, req) - if err != nil { - m.log.Error("error building cache key for request deduplication, skipping request deduplication", "error", err) - return m.BaseHandler.CallResource(ctx, req, sender) - } - _, err, _ = m.singleflight.Do(key, func() (interface{}, error) { - return nil, m.BaseHandler.CallResource(ctx, req, sender) - }) - if err != nil { - return fmt.Errorf("request deduplication middleware: calling BaseHandler.CallResource: %w", err) - } - return nil } diff --git a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go index 8185a9c66c3..f0e985b3047 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go @@ -4,10 +4,7 @@ import ( "context" "encoding/json" "net/http" - "sync" - "sync/atomic" "testing" - "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" @@ -25,9 +22,10 @@ func TestCachingMiddleware(t *testing.T) { require.NoError(t, err) cs := caching.NewFakeOSSCachingService() + cachingServiceClient := caching.ProvideCachingServiceClient(cs, nil) cdt := handlertest.NewHandlerMiddlewareTest(t, WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewCachingMiddleware(cs)), + handlertest.WithMiddlewares(NewCachingMiddleware(cachingServiceClient)), ) jsonDataMap := map[string]any{} @@ -78,9 +76,9 @@ func TestCachingMiddleware(t *testing.T) { }) t.Run("If cache returns a miss, queries are issued and the update cache function is called", func(t *testing.T) { - origShouldCacheQuery := shouldCacheQuery + origShouldCacheQuery := caching.ShouldCacheQuery var shouldCacheQueryCalled bool - shouldCacheQuery = func(resp *backend.QueryDataResponse) bool { + caching.ShouldCacheQuery = func(resp *backend.QueryDataResponse) bool { shouldCacheQueryCalled = true return true } @@ -88,7 +86,7 @@ func TestCachingMiddleware(t *testing.T) { t.Cleanup(func() { updateCacheCalled = false shouldCacheQueryCalled = false - shouldCacheQuery = origShouldCacheQuery + caching.ShouldCacheQuery = origShouldCacheQuery cs.Reset() }) @@ -108,15 +106,16 @@ func TestCachingMiddleware(t *testing.T) { }) t.Run("with async queries", func(t *testing.T) { + cachingServiceClient := caching.ProvideCachingServiceClient(cs, featuremgmt.WithFeatures(featuremgmt.FlagAwsAsyncQueryCaching)) asyncCdt := handlertest.NewHandlerMiddlewareTest(t, WithReqContext(req, &user.SignedInUser{}), handlertest.WithMiddlewares( - NewCachingMiddlewareWithFeatureManager(cs, featuremgmt.WithFeatures(featuremgmt.FlagAwsAsyncQueryCaching))), + NewCachingMiddleware(cachingServiceClient)), ) t.Run("If shoudCacheQuery returns true update cache function is called", func(t *testing.T) { - origShouldCacheQuery := shouldCacheQuery + origShouldCacheQuery := caching.ShouldCacheQuery var shouldCacheQueryCalled bool - shouldCacheQuery = func(resp *backend.QueryDataResponse) bool { + caching.ShouldCacheQuery = func(resp *backend.QueryDataResponse) bool { shouldCacheQueryCalled = true return true } @@ -124,7 +123,7 @@ func TestCachingMiddleware(t *testing.T) { t.Cleanup(func() { updateCacheCalled = false shouldCacheQueryCalled = false - shouldCacheQuery = origShouldCacheQuery + caching.ShouldCacheQuery = origShouldCacheQuery cs.Reset() }) @@ -144,9 +143,9 @@ func TestCachingMiddleware(t *testing.T) { }) t.Run("If shoudCacheQuery returns false update cache function is not called", func(t *testing.T) { - origShouldCacheQuery := shouldCacheQuery + origShouldCacheQuery := caching.ShouldCacheQuery var shouldCacheQueryCalled bool - shouldCacheQuery = func(resp *backend.QueryDataResponse) bool { + caching.ShouldCacheQuery = func(resp *backend.QueryDataResponse) bool { shouldCacheQueryCalled = true return false } @@ -154,7 +153,7 @@ func TestCachingMiddleware(t *testing.T) { t.Cleanup(func() { updateCacheCalled = false shouldCacheQueryCalled = false - shouldCacheQuery = origShouldCacheQuery + caching.ShouldCacheQuery = origShouldCacheQuery cs.Reset() }) @@ -199,9 +198,10 @@ func TestCachingMiddleware(t *testing.T) { } cs := caching.NewFakeOSSCachingService() + cachingServiceClient := caching.ProvideCachingServiceClient(cs, nil) cdt := handlertest.NewHandlerMiddlewareTest(t, WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewCachingMiddleware(cs)), + handlertest.WithMiddlewares(NewCachingMiddleware(cachingServiceClient)), handlertest.WithResourceResponses([]*backend.CallResourceResponse{simulatedPluginResponse}), ) @@ -275,9 +275,10 @@ func TestCachingMiddleware(t *testing.T) { require.NoError(t, err) cs := caching.NewFakeOSSCachingService() + cachingServiceClient := caching.ProvideCachingServiceClient(cs, nil) cdt := handlertest.NewHandlerMiddlewareTest(t, // Skip the request context in this case - handlertest.WithMiddlewares(NewCachingMiddleware(cs)), + handlertest.WithMiddlewares(NewCachingMiddleware(cachingServiceClient)), ) reqCtx := contexthandler.FromContext(req.Context()) require.Nil(t, reqCtx) @@ -325,86 +326,3 @@ func TestCachingMiddleware(t *testing.T) { }) }) } - -func TestRequestDeduplicationMiddleware(t *testing.T) { - t.Parallel() - - t.Run("deduplicates requests issuing the same query", func(t *testing.T) { - t.Parallel() - - handler := newMockMiddlewareHandler() - middleware := newRequestDeduplicationMiddleware(nil, handler) - - req := backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ - UID: "uid", - }, - }, - } - - wg := &sync.WaitGroup{} - wg.Add(2) - - for range 2 { - go func() { - defer wg.Done() - resp, err := middleware.QueryData(t.Context(), &req) - require.NoError(t, err) - require.Equal(t, &backend.QueryDataResponse{}, resp) - }() - } - - wg.Wait() - - require.EqualValues(t, 1, handler.QueryDataCalls) - }) - - t.Run("requests where DataSourceInstanceSettings is nil bypass request deduplication", func(t *testing.T) { - t.Parallel() - - handler := newMockMiddlewareHandler() - middleware := newRequestDeduplicationMiddleware(nil, handler) - - { - req := backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ - DataSourceInstanceSettings: nil, - }, - } - - resp, err := middleware.QueryData(t.Context(), &req) - require.NoError(t, err) - require.Empty(t, resp) - } - - { - req := backend.CallResourceRequest{ - PluginContext: backend.PluginContext{ - DataSourceInstanceSettings: nil, - }, - } - - require.NoError(t, middleware.CallResource(t.Context(), &req, nil)) - } - }) -} - -type mockMiddlewareHandler struct { - backend.BaseHandler - QueryDataCalls int32 -} - -func newMockMiddlewareHandler() *mockMiddlewareHandler { - return &mockMiddlewareHandler{} -} - -func (m *mockMiddlewareHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - atomic.AddInt32(&m.QueryDataCalls, 1) - time.Sleep(10 * time.Millisecond) - return &backend.QueryDataResponse{}, nil -} - -func (m *mockMiddlewareHandler) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - return nil -} diff --git a/pkg/services/pluginsintegration/installsync/syncer.go b/pkg/services/pluginsintegration/installsync/syncer.go index ac6d6ba3d32..1f056a6e85b 100644 --- a/pkg/services/pluginsintegration/installsync/syncer.go +++ b/pkg/services/pluginsintegration/installsync/syncer.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana/apps/plugins/pkg/app/install" @@ -90,6 +91,11 @@ func (s *syncer) Sync(ctx context.Context, source install.Source, installedPlugi return nil } + if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginStoreServiceLoading) { + logging.DefaultLogger.Warn("pluginInstallAPISync is enabled, but pluginStoreServiceLoading is disabled. skipping plugin sync.") + return nil + } + if len(installedPlugins) == 0 { return nil } diff --git a/pkg/services/pluginsintegration/installsync/syncer_test.go b/pkg/services/pluginsintegration/installsync/syncer_test.go index a1ca40638b0..e8894436083 100644 --- a/pkg/services/pluginsintegration/installsync/syncer_test.go +++ b/pkg/services/pluginsintegration/installsync/syncer_test.go @@ -20,234 +20,68 @@ import ( "github.com/grafana/grafana/pkg/services/org/orgtest" ) -// Test helpers to avoid import cycles -type fakeServerLock struct { - lockFunc func(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error -} - -func (f *fakeServerLock) LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error { - if f.lockFunc != nil { - return f.lockFunc(ctx, actionName, maxInterval, fn) - } - fn(ctx) - return nil -} - -type fakePluginInstallClient struct { - listAllFunc func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) - getFunc func(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.PluginInstall, error) - createFunc func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) - updateFunc func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.UpdateOptions) (*pluginsv0alpha1.PluginInstall, error) - deleteFunc func(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error -} - -func (f *fakePluginInstallClient) Get(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.PluginInstall, error) { - if f.getFunc != nil { - return f.getFunc(ctx, identifier) - } - // Return a proper k8s NotFound error - return nil, errorsK8s.NewNotFound(schema.GroupResource{ - Group: pluginsv0alpha1.APIGroup, - Resource: "plugininstalls", - }, identifier.Name) -} - -func (f *fakePluginInstallClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) { - if f.listAllFunc != nil { - return f.listAllFunc(ctx, namespace, opts) - } - return &pluginsv0alpha1.PluginInstallList{}, nil -} - -func (f *fakePluginInstallClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) { - return f.ListAll(ctx, namespace, opts) -} - -func (f *fakePluginInstallClient) Create(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) { - if f.createFunc != nil { - return f.createFunc(ctx, obj, opts) - } - return obj, nil -} - -func (f *fakePluginInstallClient) Update(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.UpdateOptions) (*pluginsv0alpha1.PluginInstall, error) { - if f.updateFunc != nil { - return f.updateFunc(ctx, obj, opts) - } - return obj, nil -} - -func (f *fakePluginInstallClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus pluginsv0alpha1.PluginInstallStatus, opts resource.UpdateOptions) (*pluginsv0alpha1.PluginInstall, error) { - return nil, nil -} - -func (f *fakePluginInstallClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*pluginsv0alpha1.PluginInstall, error) { - return nil, nil -} - -func (f *fakePluginInstallClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { - if f.deleteFunc != nil { - return f.deleteFunc(ctx, identifier, opts) - } - return nil -} - -type fakeClientGenerator struct { - client *fakePluginInstallClient -} - -func (f *fakeClientGenerator) ClientFor(kind resource.Kind) (resource.Client, error) { - return &fakeResourceClient{client: f.client}, nil -} - -type fakeResourceClient struct { - client *fakePluginInstallClient -} - -func (f *fakeResourceClient) Get(ctx context.Context, identifier resource.Identifier) (resource.Object, error) { - return f.client.Get(ctx, identifier) -} - -func (f *fakeResourceClient) GetInto(ctx context.Context, identifier resource.Identifier, into resource.Object) error { - obj, err := f.client.Get(ctx, identifier) - if err != nil { - return err - } - // Copy the object data into the provided 'into' object - if target, ok := into.(*pluginsv0alpha1.PluginInstall); ok { - *target = *obj - } - return nil -} - -func (f *fakeResourceClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { - return f.client.ListAll(ctx, namespace, options) -} - -func (f *fakeResourceClient) ListInto(ctx context.Context, namespace string, options resource.ListOptions, into resource.ListObject) error { - list, err := f.client.ListAll(ctx, namespace, options) - if err != nil { - return err - } - // Copy the list data into the provided 'into' object - if target, ok := into.(*pluginsv0alpha1.PluginInstallList); ok { - *target = *list - } - return nil -} - -func (f *fakeResourceClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) { - plugin := obj.(*pluginsv0alpha1.PluginInstall) - return f.client.Create(ctx, plugin, options) -} - -func (f *fakeResourceClient) CreateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions, into resource.Object) error { - created, err := f.Create(ctx, identifier, obj, options) - if err != nil { - return err - } - // Copy the created object data into the provided 'into' object - if plugin, ok := created.(*pluginsv0alpha1.PluginInstall); ok { - if target, ok := into.(*pluginsv0alpha1.PluginInstall); ok { - *target = *plugin - } - } - return nil -} - -func (f *fakeResourceClient) Update(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions) (resource.Object, error) { - plugin := obj.(*pluginsv0alpha1.PluginInstall) - return f.client.Update(ctx, plugin, options) -} - -func (f *fakeResourceClient) UpdateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions, into resource.Object) error { - updated, err := f.Update(ctx, identifier, obj, options) - if err != nil { - return err - } - // Copy the updated object data into the provided 'into' object - if plugin, ok := updated.(*pluginsv0alpha1.PluginInstall); ok { - if target, ok := into.(*pluginsv0alpha1.PluginInstall); ok { - *target = *plugin - } - } - return nil -} - -func (f *fakeResourceClient) Patch(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions) (resource.Object, error) { - return f.client.Patch(ctx, identifier, patch, options) -} - -func (f *fakeResourceClient) PatchInto(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions, into resource.Object) error { - patched, err := f.Patch(ctx, identifier, patch, options) - if err != nil { - return err - } - // Copy the patched object data into the provided 'into' object - if plugin, ok := patched.(*pluginsv0alpha1.PluginInstall); ok { - if target, ok := into.(*pluginsv0alpha1.PluginInstall); ok { - *target = *plugin - } - } - return nil -} - -func (f *fakeResourceClient) Delete(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error { - return f.client.Delete(ctx, identifier, options) -} - -func (f *fakeResourceClient) SubresourceRequest(ctx context.Context, identifier resource.Identifier, req resource.CustomRouteRequestOptions) ([]byte, error) { - return []byte{}, nil -} - -func (f *fakeResourceClient) Watch(ctx context.Context, namespace string, options resource.WatchOptions) (resource.WatchResponse, error) { - return &fakeWatchResponse{}, nil -} - -type fakeWatchResponse struct{} - -func (f *fakeWatchResponse) Stop() {} - -func (f *fakeWatchResponse) WatchEvents() <-chan resource.WatchEvent { - ch := make(chan resource.WatchEvent) - close(ch) - return ch -} - func TestSyncer_Sync(t *testing.T) { tests := []struct { - name string - featureToggleEnabled bool - orgs []*org.OrgDTO - orgServiceError error - serverLockError error - expectedError error - expectSyncCalls int + name string + pluginInstallAPISyncEnabled bool + pluginStoreServiceEnabled bool + installedPlugins []*plugins.Plugin + orgs []*org.OrgDTO + orgServiceError error + serverLockError error + expectedError error + expectSyncCalls int }{ { - name: "feature toggle disabled", - featureToggleEnabled: false, - orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}}, - expectedError: nil, - expectSyncCalls: 0, + name: "plugin install API sync feature toggle disabled", + pluginInstallAPISyncEnabled: false, + pluginStoreServiceEnabled: true, + installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}}, + orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}}, + expectedError: nil, + expectSyncCalls: 0, }, { - name: "feature toggle enabled, no orgs", - featureToggleEnabled: true, - orgs: []*org.OrgDTO{}, - expectedError: nil, - expectSyncCalls: 0, + name: "plugin store service feature toggle disabled", + pluginInstallAPISyncEnabled: true, + pluginStoreServiceEnabled: false, + installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}}, + orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}}, + expectedError: nil, + expectSyncCalls: 0, }, { - name: "feature toggle enabled, single org", - featureToggleEnabled: true, - orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}}, - expectedError: nil, - expectSyncCalls: 1, + name: "both feature toggles enabled, no orgs", + pluginInstallAPISyncEnabled: true, + pluginStoreServiceEnabled: true, + installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}}, + orgs: []*org.OrgDTO{}, + expectedError: nil, + expectSyncCalls: 0, }, { - name: "feature toggle enabled, multiple orgs", - featureToggleEnabled: true, + name: "both feature toggles enabled, empty installed plugins", + pluginInstallAPISyncEnabled: true, + pluginStoreServiceEnabled: true, + installedPlugins: []*plugins.Plugin{}, + orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}}, + expectedError: nil, + expectSyncCalls: 0, + }, + { + name: "both feature toggles enabled, single org", + pluginInstallAPISyncEnabled: true, + pluginStoreServiceEnabled: true, + installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}}, + orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}}, + expectedError: nil, + expectSyncCalls: 1, + }, + { + name: "both feature toggles enabled, multiple orgs", + pluginInstallAPISyncEnabled: true, + pluginStoreServiceEnabled: true, + installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}}, orgs: []*org.OrgDTO{ {ID: 1, Name: "Org 1"}, {ID: 2, Name: "Org 2"}, @@ -257,20 +91,24 @@ func TestSyncer_Sync(t *testing.T) { expectSyncCalls: 3, }, { - name: "org service error", - featureToggleEnabled: true, - orgs: nil, - orgServiceError: errors.New("org service error"), - expectedError: errors.New("org service error"), - expectSyncCalls: 0, + name: "org service error", + pluginInstallAPISyncEnabled: true, + pluginStoreServiceEnabled: true, + installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}}, + orgs: nil, + orgServiceError: errors.New("org service error"), + expectedError: errors.New("org service error"), + expectSyncCalls: 0, }, { - name: "server lock error", - featureToggleEnabled: true, - orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}}, - serverLockError: errors.New("lock error"), - expectedError: errors.New("lock error"), - expectSyncCalls: 0, + name: "server lock error", + pluginInstallAPISyncEnabled: true, + pluginStoreServiceEnabled: true, + installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}}, + orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}}, + serverLockError: errors.New("lock error"), + expectedError: errors.New("lock error"), + expectSyncCalls: 0, }, } @@ -280,7 +118,8 @@ func TestSyncer_Sync(t *testing.T) { // Setup feature toggles ft := featuremgmt.NewMockFeatureToggles(t) - ft.EXPECT().IsEnabled(ctx, featuremgmt.FlagPluginInstallAPISync).Return(tt.featureToggleEnabled).Maybe() + ft.EXPECT().IsEnabled(ctx, featuremgmt.FlagPluginInstallAPISync).Return(tt.pluginInstallAPISyncEnabled).Maybe() + ft.EXPECT().IsEnabled(ctx, featuremgmt.FlagPluginStoreServiceLoading).Return(tt.pluginStoreServiceEnabled).Maybe() // Setup org service orgService := orgtest.NewOrgServiceFake() @@ -298,12 +137,12 @@ func TestSyncer_Sync(t *testing.T) { // Setup fake client and registrar syncCalls := 0 fakeClient := &fakePluginInstallClient{ - createFunc: func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) { + createFunc: func(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) { syncCalls++ return obj, nil }, - listAllFunc: func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) { - return &pluginsv0alpha1.PluginInstallList{}, nil + listAllFunc: func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) { + return &pluginsv0alpha1.PluginList{}, nil }, } clientGen := &fakeClientGenerator{client: fakeClient} @@ -320,10 +159,7 @@ func TestSyncer_Sync(t *testing.T) { ) // Execute - installedPlugins := []*plugins.Plugin{ - {JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}}, - } - err := s.Sync(ctx, install.SourcePluginStore, installedPlugins) + err := s.Sync(ctx, install.SourcePluginStore, tt.installedPlugins) // Verify if tt.expectedError != nil { @@ -342,7 +178,7 @@ func TestSyncer_syncNamespace(t *testing.T) { tests := []struct { name string installedPlugins []*plugins.Plugin - apiPlugins []pluginsv0alpha1.PluginInstall + apiPlugins []pluginsv0alpha1.Plugin clientListError error expectedError error expectedRegCalls int @@ -353,7 +189,7 @@ func TestSyncer_syncNamespace(t *testing.T) { { name: "no installed plugins, no API plugins", installedPlugins: []*plugins.Plugin{}, - apiPlugins: []pluginsv0alpha1.PluginInstall{}, + apiPlugins: []pluginsv0alpha1.Plugin{}, expectedError: nil, expectedRegCalls: 0, expectedUnregCalls: 0, @@ -364,7 +200,7 @@ func TestSyncer_syncNamespace(t *testing.T) { {JSONData: plugins.JSONData{ID: "plugin-1", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}, {JSONData: plugins.JSONData{ID: "plugin-2", Info: plugins.Info{Version: "2.0.0"}}, Class: plugins.ClassExternal}, }, - apiPlugins: []pluginsv0alpha1.PluginInstall{}, + apiPlugins: []pluginsv0alpha1.Plugin{}, expectedError: nil, expectedRegCalls: 2, expectedUnregCalls: 0, @@ -373,7 +209,7 @@ func TestSyncer_syncNamespace(t *testing.T) { { name: "API plugins only", installedPlugins: []*plugins.Plugin{}, - apiPlugins: []pluginsv0alpha1.PluginInstall{ + apiPlugins: []pluginsv0alpha1.Plugin{ { ObjectMeta: metav1.ObjectMeta{ Name: "plugin-1", @@ -381,7 +217,7 @@ func TestSyncer_syncNamespace(t *testing.T) { install.PluginInstallSourceAnnotation: install.SourcePluginStore, }, }, - Spec: pluginsv0alpha1.PluginInstallSpec{Id: "plugin-1"}, + Spec: pluginsv0alpha1.PluginSpec{Id: "plugin-1"}, }, { ObjectMeta: metav1.ObjectMeta{ @@ -390,7 +226,7 @@ func TestSyncer_syncNamespace(t *testing.T) { install.PluginInstallSourceAnnotation: install.SourcePluginStore, }, }, - Spec: pluginsv0alpha1.PluginInstallSpec{Id: "plugin-2"}, + Spec: pluginsv0alpha1.PluginSpec{Id: "plugin-2"}, }, }, expectedError: nil, @@ -405,7 +241,7 @@ func TestSyncer_syncNamespace(t *testing.T) { {JSONData: plugins.JSONData{ID: "plugin-2", Info: plugins.Info{Version: "2.0.0"}}, Class: plugins.ClassExternal}, {JSONData: plugins.JSONData{ID: "plugin-3", Info: plugins.Info{Version: "3.0.0"}}, Class: plugins.ClassExternal}, }, - apiPlugins: []pluginsv0alpha1.PluginInstall{ + apiPlugins: []pluginsv0alpha1.Plugin{ { ObjectMeta: metav1.ObjectMeta{ Name: "plugin-2", @@ -413,7 +249,7 @@ func TestSyncer_syncNamespace(t *testing.T) { install.PluginInstallSourceAnnotation: install.SourcePluginStore, }, }, - Spec: pluginsv0alpha1.PluginInstallSpec{Id: "plugin-2", Version: "2.0.0"}, + Spec: pluginsv0alpha1.PluginSpec{Id: "plugin-2", Version: "2.0.0"}, }, { ObjectMeta: metav1.ObjectMeta{ @@ -422,7 +258,7 @@ func TestSyncer_syncNamespace(t *testing.T) { install.PluginInstallSourceAnnotation: install.SourcePluginStore, }, }, - Spec: pluginsv0alpha1.PluginInstallSpec{Id: "plugin-4"}, + Spec: pluginsv0alpha1.PluginSpec{Id: "plugin-4"}, }, }, expectedError: nil, @@ -434,7 +270,7 @@ func TestSyncer_syncNamespace(t *testing.T) { { name: "list error", installedPlugins: []*plugins.Plugin{}, - apiPlugins: []pluginsv0alpha1.PluginInstall{}, + apiPlugins: []pluginsv0alpha1.Plugin{}, clientListError: errors.New("list error"), expectedError: errors.New("list error"), }, @@ -450,15 +286,15 @@ func TestSyncer_syncNamespace(t *testing.T) { // Setup fake client fakeClient := &fakePluginInstallClient{ - listAllFunc: func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) { + listAllFunc: func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) { if tt.clientListError != nil { return nil, tt.clientListError } - return &pluginsv0alpha1.PluginInstallList{ + return &pluginsv0alpha1.PluginList{ Items: tt.apiPlugins, }, nil }, - createFunc: func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) { + createFunc: func(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) { registeredIDs = append(registeredIDs, obj.Spec.Id) return obj, nil }, @@ -466,7 +302,7 @@ func TestSyncer_syncNamespace(t *testing.T) { unregisteredIDs = append(unregisteredIDs, identifier.Name) return nil }, - getFunc: func(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.PluginInstall, error) { + getFunc: func(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.Plugin, error) { // Check if plugin exists in apiPlugins for i := range tt.apiPlugins { if tt.apiPlugins[i].Name == identifier.Name { @@ -521,7 +357,7 @@ func TestSyncer_syncNamespace(t *testing.T) { } } -func TestSyncer_getClient(t *testing.T) { +func TestInstallRegistrar_GetClient(t *testing.T) { tests := []struct { name string }{ @@ -559,92 +395,196 @@ func TestSyncer_getClient(t *testing.T) { } } -func TestSyncer_syncAllNamespaces(t *testing.T) { - tests := []struct { - name string - orgs []*org.OrgDTO - orgServiceError error - expectedError error - expectedCalls int - }{ - { - name: "no orgs", - orgs: []*org.OrgDTO{}, - expectedError: nil, - expectedCalls: 0, - }, - { - name: "single org", - orgs: []*org.OrgDTO{ - {ID: 1, Name: "Org 1"}, - }, - expectedError: nil, - expectedCalls: 1, - }, - { - name: "multiple orgs", - orgs: []*org.OrgDTO{ - {ID: 1, Name: "Org 1"}, - {ID: 2, Name: "Org 2"}, - {ID: 3, Name: "Org 3"}, - }, - expectedError: nil, - expectedCalls: 3, - }, - { - name: "org service error", - orgs: nil, - orgServiceError: errors.New("org service error"), - expectedError: errors.New("org service error"), - expectedCalls: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - - orgService := orgtest.NewOrgServiceFake() - orgService.ExpectedOrgs = tt.orgs - orgService.ExpectedError = tt.orgServiceError - - // Track namespace sync calls - syncCalls := 0 - fakeClient := &fakePluginInstallClient{ - createFunc: func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) { - syncCalls++ - return obj, nil - }, - listAllFunc: func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) { - return &pluginsv0alpha1.PluginInstallList{}, nil - }, - } - - clientGen := &fakeClientGenerator{client: fakeClient} - - s := newSyncer( - featuremgmt.NewMockFeatureToggles(t), - clientGen, - install.NewInstallRegistrar(clientGen), - orgService, - func(orgID int64) string { return "org-1" }, - &fakeServerLock{}, - ) - - installedPlugins := []*plugins.Plugin{ - {JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}, - } - - err := s.syncAllNamespaces(ctx, install.SourcePluginStore, installedPlugins) - - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - } - - require.Equal(t, tt.expectedCalls, syncCalls) - }) +// Test helpers to avoid import cycles +type fakeServerLock struct { + lockFunc func(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error +} + +func (f *fakeServerLock) LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error { + if f.lockFunc != nil { + return f.lockFunc(ctx, actionName, maxInterval, fn) + } + fn(ctx) + return nil +} + +type fakePluginInstallClient struct { + listAllFunc func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) + getFunc func(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.Plugin, error) + createFunc func(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) + updateFunc func(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) + deleteFunc func(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error +} + +func (f *fakePluginInstallClient) Get(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.Plugin, error) { + if f.getFunc != nil { + return f.getFunc(ctx, identifier) + } + // Return a proper k8s NotFound error + return nil, errorsK8s.NewNotFound(schema.GroupResource{ + Group: pluginsv0alpha1.APIGroup, + Resource: "plugininstalls", + }, identifier.Name) +} + +func (f *fakePluginInstallClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) { + if f.listAllFunc != nil { + return f.listAllFunc(ctx, namespace, opts) + } + return &pluginsv0alpha1.PluginList{}, nil +} + +func (f *fakePluginInstallClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) { + return f.ListAll(ctx, namespace, opts) +} + +func (f *fakePluginInstallClient) Create(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) { + if f.createFunc != nil { + return f.createFunc(ctx, obj, opts) + } + return obj, nil +} + +func (f *fakePluginInstallClient) Update(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) { + if f.updateFunc != nil { + return f.updateFunc(ctx, obj, opts) + } + return obj, nil +} + +func (f *fakePluginInstallClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus pluginsv0alpha1.PluginStatus, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) { + return nil, nil +} + +func (f *fakePluginInstallClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*pluginsv0alpha1.Plugin, error) { + return nil, nil +} + +func (f *fakePluginInstallClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + if f.deleteFunc != nil { + return f.deleteFunc(ctx, identifier, opts) + } + return nil +} + +type fakeClientGenerator struct { + client *fakePluginInstallClient +} + +func (f *fakeClientGenerator) ClientFor(kind resource.Kind) (resource.Client, error) { + return &fakeResourceClient{client: f.client}, nil +} + +type fakeResourceClient struct { + client *fakePluginInstallClient +} + +func (f *fakeResourceClient) Get(ctx context.Context, identifier resource.Identifier) (resource.Object, error) { + return f.client.Get(ctx, identifier) +} + +func (f *fakeResourceClient) GetInto(ctx context.Context, identifier resource.Identifier, into resource.Object) error { + obj, err := f.client.Get(ctx, identifier) + if err != nil { + return err + } + // Copy the object data into the provided 'into' object + if target, ok := into.(*pluginsv0alpha1.Plugin); ok { + *target = *obj } + return nil +} + +func (f *fakeResourceClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + return f.client.ListAll(ctx, namespace, options) +} + +func (f *fakeResourceClient) ListInto(ctx context.Context, namespace string, options resource.ListOptions, into resource.ListObject) error { + list, err := f.client.ListAll(ctx, namespace, options) + if err != nil { + return err + } + // Copy the list data into the provided 'into' object + if target, ok := into.(*pluginsv0alpha1.PluginList); ok { + *target = *list + } + return nil +} + +func (f *fakeResourceClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) { + plugin := obj.(*pluginsv0alpha1.Plugin) + return f.client.Create(ctx, plugin, options) +} + +func (f *fakeResourceClient) CreateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions, into resource.Object) error { + created, err := f.Create(ctx, identifier, obj, options) + if err != nil { + return err + } + // Copy the created object data into the provided 'into' object + if plugin, ok := created.(*pluginsv0alpha1.Plugin); ok { + if target, ok := into.(*pluginsv0alpha1.Plugin); ok { + *target = *plugin + } + } + return nil +} + +func (f *fakeResourceClient) Update(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions) (resource.Object, error) { + plugin := obj.(*pluginsv0alpha1.Plugin) + return f.client.Update(ctx, plugin, options) +} + +func (f *fakeResourceClient) UpdateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions, into resource.Object) error { + updated, err := f.Update(ctx, identifier, obj, options) + if err != nil { + return err + } + // Copy the updated object data into the provided 'into' object + if plugin, ok := updated.(*pluginsv0alpha1.Plugin); ok { + if target, ok := into.(*pluginsv0alpha1.Plugin); ok { + *target = *plugin + } + } + return nil +} + +func (f *fakeResourceClient) Patch(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions) (resource.Object, error) { + return f.client.Patch(ctx, identifier, patch, options) +} + +func (f *fakeResourceClient) PatchInto(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions, into resource.Object) error { + patched, err := f.Patch(ctx, identifier, patch, options) + if err != nil { + return err + } + // Copy the patched object data into the provided 'into' object + if plugin, ok := patched.(*pluginsv0alpha1.Plugin); ok { + if target, ok := into.(*pluginsv0alpha1.Plugin); ok { + *target = *plugin + } + } + return nil +} + +func (f *fakeResourceClient) Delete(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error { + return f.client.Delete(ctx, identifier, options) +} + +func (f *fakeResourceClient) SubresourceRequest(ctx context.Context, identifier resource.Identifier, req resource.CustomRouteRequestOptions) ([]byte, error) { + return []byte{}, nil +} + +func (f *fakeResourceClient) Watch(ctx context.Context, namespace string, options resource.WatchOptions) (resource.WatchResponse, error) { + return &fakeWatchResponse{}, nil +} + +type fakeWatchResponse struct{} + +func (f *fakeWatchResponse) Stop() {} + +func (f *fakeWatchResponse) WatchEvents() <-chan resource.WatchEvent { + ch := make(chan resource.WatchEvent) + close(ch) + return ch } diff --git a/pkg/services/pluginsintegration/plugininstaller/service.go b/pkg/services/pluginsintegration/plugininstaller/service.go index 7232703820c..670123e5743 100644 --- a/pkg/services/pluginsintegration/plugininstaller/service.go +++ b/pkg/services/pluginsintegration/plugininstaller/service.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/repo" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/setting" @@ -45,7 +44,6 @@ type Service struct { pluginInstaller plugins.Installer pluginStore pluginstore.Store pluginRepo repo.Service - features featuremgmt.FeatureToggles updateChecker pluginchecker.PluginUpdateChecker installComplete chan struct{} // closed when all plugins are installed (used for testing) } @@ -56,7 +54,6 @@ func ProvideService( pluginInstaller plugins.Installer, promReg prometheus.Registerer, pluginRepo repo.Service, - features featuremgmt.FeatureToggles, updateChecker pluginchecker.PluginUpdateChecker, ) (*Service, error) { once.Do(func() { @@ -70,7 +67,6 @@ func ProvideService( pluginInstaller: pluginInstaller, pluginStore: pluginStore, pluginRepo: pluginRepo, - features: features, updateChecker: updateChecker, installComplete: make(chan struct{}), } @@ -111,8 +107,8 @@ func (s *Service) installPlugins(ctx context.Context, pluginsToInstall []setting continue } if installPlugin.Version == "" { - if !s.features.IsEnabled(ctx, featuremgmt.FlagPreinstallAutoUpdate) { - // Skip updating the plugin if the feature flag is disabled + if !s.cfg.PreinstallAutoUpdate { + // Skip updating the plugin if auto-update is disabled continue } // The plugin is installed but it's not pinned to a specific version diff --git a/pkg/services/pluginsintegration/plugininstaller/service_test.go b/pkg/services/pluginsintegration/plugininstaller/service_test.go index c832ff06ee5..f0d34cfc660 100644 --- a/pkg/services/pluginsintegration/plugininstaller/service_test.go +++ b/pkg/services/pluginsintegration/plugininstaller/service_test.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/repo" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes" "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" @@ -31,7 +30,6 @@ func TestService_IsDisabled(t *testing.T) { &pluginfakes.FakePluginInstaller{}, prometheus.NewRegistry(), &pluginfakes.FakePluginRepo{}, - featuremgmt.WithFeatures(), &pluginchecker.FakePluginUpdateChecker{}, ) require.NoError(t, err) @@ -167,6 +165,7 @@ func TestService_Run(t *testing.T) { &setting.Cfg{ PreinstallPluginsAsync: tt.pluginsToInstall, PreinstallPluginsSync: tt.pluginsToInstallSync, + PreinstallAutoUpdate: true, }, store, &pluginfakes.FakePluginInstaller{ @@ -199,7 +198,6 @@ func TestService_Run(t *testing.T) { return tt.latestPlugin, nil }, }, - featuremgmt.WithFeatures(featuremgmt.FlagPreinstallAutoUpdate), pluginchecker.ProvideService( managedplugins.NewNoop(), provisionedplugins.NewNoop(), diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 46bc10cb76b..a510ead1910 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -167,25 +167,25 @@ func ProvideClientWithMiddlewares( pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, - cachingService caching.CachingService, + cachingServiceClient *caching.CachingServiceClient, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, ) (*backend.MiddlewareHandler, error) { - return NewMiddlewareHandler(cfg, pluginRegistry, oAuthTokenService, tracer, cachingService, features, promRegisterer, pluginRegistry) + return NewMiddlewareHandler(cfg, pluginRegistry, oAuthTokenService, tracer, cachingServiceClient, features, promRegisterer, pluginRegistry) } func NewMiddlewareHandler( cfg *setting.Cfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, - tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, + tracer tracing.Tracer, cachingServiceClient *caching.CachingServiceClient, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service, ) (*backend.MiddlewareHandler, error) { c := client.ProvideService(pluginRegistry) - middlewares := CreateMiddlewares(cfg, oAuthTokenService, tracer, cachingService, features, promRegisterer, registry) + middlewares := CreateMiddlewares(cfg, oAuthTokenService, tracer, cachingServiceClient, features, promRegisterer, registry) 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) []backend.HandlerMiddleware { +func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingServiceClient *caching.CachingServiceClient, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service) []backend.HandlerMiddleware { middlewares := []backend.HandlerMiddleware{ clientmiddleware.NewTracingMiddleware(tracer), clientmiddleware.NewMetricsMiddleware(promRegisterer, registry), @@ -203,7 +203,7 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken clientmiddleware.NewClearAuthHeadersMiddleware(), clientmiddleware.NewOAuthTokenMiddleware(oAuthTokenService), clientmiddleware.NewCookiesMiddleware(skipCookiesNames), - clientmiddleware.NewCachingMiddlewareWithFeatureManager(cachingService, features), + clientmiddleware.NewCachingMiddleware(cachingServiceClient), clientmiddleware.NewForwardIDMiddleware(), clientmiddleware.NewUseAlertHeadersMiddleware(), ) diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 717326305ae..db67f0dccd9 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -69,11 +69,17 @@ func createDashboardJSON(data *simplejson.Json, lastModified time.Time, cfg *con dash.Dashboard = dashboards.NewDashboardFromJson(data) dash.UpdatedAt = lastModified dash.Overwrite = true + if dash.Dashboard.OrgID > 0 && dash.Dashboard.OrgID != cfg.OrgID { + return nil, fmt.Errorf("dashboard orgID (%d) does not match provisioning provider orgID (%d)", dash.Dashboard.OrgID, cfg.OrgID) + } dash.OrgID = cfg.OrgID dash.Dashboard.OrgID = cfg.OrgID metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Provisioning).Inc() // nolint:staticcheck dash.Dashboard.FolderID = folderID + if dash.Dashboard.FolderUID != "" && folderUID != dash.Dashboard.FolderUID { + return nil, fmt.Errorf("dashboard folderUID (%q) does not match provisioning provider folderUID (%q)", dash.Dashboard.FolderUID, folderUID) + } dash.Dashboard.FolderUID = folderUID if dash.Dashboard.Title == "" { diff --git a/pkg/services/provisioning/dashboards/types_test.go b/pkg/services/provisioning/dashboards/types_test.go new file mode 100644 index 00000000000..49382d3561a --- /dev/null +++ b/pkg/services/provisioning/dashboards/types_test.go @@ -0,0 +1,193 @@ +package dashboards + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateDashboardJSON(t *testing.T) { + lastModified := time.Now() + folderID := int64(123) + folderUID := "folder-uid-123" + + t.Run("orgID check", func(t *testing.T) { + t.Run("matching is OK", func(t *testing.T) { + cfg := &config{ + OrgID: 1, + } + + dashboardJSON := simplejson.NewFromAny(map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "test-dashboard-uid", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "title": "Test Dashboard", + }, + }) + + result, err := createDashboardJSON(dashboardJSON, lastModified, cfg, folderID, folderUID) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "Test Dashboard", result.Dashboard.Title) + assert.Equal(t, int64(1), result.OrgID) + assert.Equal(t, int64(1), result.Dashboard.OrgID) + assert.Equal(t, folderID, result.Dashboard.FolderID) // nolint:staticcheck + assert.Equal(t, folderUID, result.Dashboard.FolderUID) + assert.True(t, result.Overwrite) + assert.Equal(t, lastModified, result.UpdatedAt) + }) + + t.Run("not set is OK", func(t *testing.T) { + cfg := &config{ + OrgID: 1, + } + + dashboardJSON := simplejson.NewFromAny(map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "test-dashboard-uid", + }, + "spec": map[string]interface{}{ + "title": "Test Dashboard", + }, + }) + + result, err := createDashboardJSON(dashboardJSON, lastModified, cfg, folderID, folderUID) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, int64(1), result.OrgID) + assert.Equal(t, int64(1), result.Dashboard.OrgID) + }) + + t.Run("not matching is an error", func(t *testing.T) { + cfg := &config{ + OrgID: 1, + } + + dashboardJSON := simplejson.NewFromAny(map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "test-dashboard-uid", + "namespace": "org-123", + }, + "spec": map[string]interface{}{ + "title": "Test Dashboard", + }, + }) + + result, err := createDashboardJSON(dashboardJSON, lastModified, cfg, folderID, folderUID) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "dashboard orgID") + }) + }) + + t.Run("folderUID check", func(t *testing.T) { + t.Run("matching is OK", func(t *testing.T) { + cfg := &config{ + OrgID: 1, + } + + dashboardJSON := simplejson.NewFromAny(map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "test-dashboard-uid", + "namespace": "default", + "annotations": map[string]interface{}{ + "grafana.app/folder": folderUID, + }, + }, + "spec": map[string]interface{}{ + "title": "Test Dashboard", + }, + }) + + result, err := createDashboardJSON(dashboardJSON, lastModified, cfg, folderID, folderUID) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, folderUID, result.Dashboard.FolderUID) + }) + + t.Run("not matching is an error", func(t *testing.T) { + cfg := &config{ + OrgID: 1, + } + + dashboardJSON := simplejson.NewFromAny(map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "test-dashboard-uid", + "namespace": "default", + "annotations": map[string]interface{}{ + "grafana.app/folder": "different-folder-uid", + }, + }, + "spec": map[string]interface{}{ + "title": "Test Dashboard", + }, + }) + + result, err := createDashboardJSON(dashboardJSON, lastModified, cfg, folderID, folderUID) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "dashboard folderUID") + }) + + t.Run("not set is OK", func(t *testing.T) { + cfg := &config{ + OrgID: 1, + } + + dashboardJSON := simplejson.NewFromAny(map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "test-dashboard-uid", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "title": "Test Dashboard", + }, + }) + + result, err := createDashboardJSON(dashboardJSON, lastModified, cfg, folderID, folderUID) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, folderUID, result.Dashboard.FolderUID) + }) + }) + + t.Run("empty title is an error", func(t *testing.T) { + cfg := &config{ + OrgID: 1, + } + + dashboardJSON := simplejson.NewFromAny(map[string]any{ + "title": "", + }) + + result, err := createDashboardJSON(dashboardJSON, lastModified, cfg, folderID, folderUID) + + require.Error(t, err) + assert.Nil(t, result) + assert.Equal(t, dashboards.ErrDashboardTitleEmpty, err) + }) +} diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index b54ef3ec6db..c2b39355737 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -4,9 +4,12 @@ package publicdashboards import ( context "context" + "fmt" backend "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/expr" dashboards "github.com/grafana/grafana/pkg/services/dashboards" dtos "github.com/grafana/grafana/pkg/api/dtos" @@ -587,6 +590,10 @@ func (_m *FakePublicDashboardService) Update(ctx context.Context, u *user.Signed return r0, r1 } +func (_m *FakePublicDashboardService) GetSQLSchemas(ctx context.Context, user identity.Requester, reqDTO dtos.MetricRequest) (expr.SQLSchemas, error) { + return nil, fmt.Errorf("not implemented in public dashboards") +} + // NewFakePublicDashboardService creates a new instance of FakePublicDashboardService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFakePublicDashboardService(t interface { diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index c3c2eb83496..b31657ef267 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -5,6 +5,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/expr" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -37,6 +39,8 @@ type Service interface { ExistsEnabledByAccessToken(ctx context.Context, accessToken string) (bool, error) ExistsEnabledByDashboardUid(ctx context.Context, dashboardUid string) (bool, error) + + GetSQLSchemas(ctx context.Context, user identity.Requester, reqDTO dtos.MetricRequest) (expr.SQLSchemas, error) } // ServiceWrapper these methods have different behavior between OSS and Enterprise. The latter would call the OSS service first diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 239ce31829c..bb3c70dcef4 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -512,6 +513,10 @@ func (pd *PublicDashboardServiceImpl) logIsEnabledChanged(existingPubdash *Publi } } +func (pd *PublicDashboardServiceImpl) GetSQLSchemas(ctx context.Context, user identity.Requester, reqDTO dtos.MetricRequest) (expr.SQLSchemas, error) { + return nil, fmt.Errorf("sql schema endpoint not supported with public dashboards") +} + // Checks to see if PublicDashboard.ExistsEnabledByDashboardUid is true on create or changed on update func publicDashboardIsEnabledChanged(existingPubdash *PublicDashboard, newPubdash *PublicDashboard) bool { // creating dashboard, enabled true diff --git a/pkg/services/query/expr_sql_schema.go b/pkg/services/query/expr_sql_schema.go new file mode 100644 index 00000000000..db29da98625 --- /dev/null +++ b/pkg/services/query/expr_sql_schema.go @@ -0,0 +1,75 @@ +package query + +import ( + "context" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/dsquerierclient" + "github.com/grafana/grafana/pkg/services/validations" +) + +func (s *ServiceImpl) GetSQLSchemas(ctx context.Context, user identity.Requester, reqDTO dtos.MetricRequest) (expr.SQLSchemas, error) { + //TODO DEdupe code + + parsedReq, err := s.parseMetricRequest(ctx, user, false, reqDTO, false) + if err != nil { + return expr.SQLSchemas{}, err + } + exprReq := expr.Request{ + Queries: []expr.Query{}, + } + + if user != nil { // for passthrough authentication, SSE does not authenticate + exprReq.User = user + exprReq.OrgId = user.GetOrgID() + } + + for _, pq := range parsedReq.getFlattenedQueries() { + if pq.datasource == nil { + return nil, ErrMissingDataSourceInfo.Build(errutil.TemplateData{ + Public: map[string]any{ + "RefId": pq.query.RefID, + }, + }) + } + + exprReq.Queries = append(exprReq.Queries, expr.Query{ + JSON: pq.query.JSON, + Interval: pq.query.Interval, + RefID: pq.query.RefID, + MaxDataPoints: pq.query.MaxDataPoints, + QueryType: pq.query.QueryType, + DataSource: pq.datasource, + TimeRange: expr.AbsoluteTimeRange{ + From: pq.query.TimeRange.From, + To: pq.query.TimeRange.To, + }, + }) + } + + return s.expressionService.GetSQLSchemas(ctx, exprReq) +} + +func GetSQLSchemas(ctx context.Context, log log.Logger, dscache datasources.CacheService, exprService *expr.Service, reqDTO dtos.MetricRequest, qsDatasourceClientBuilder dsquerierclient.QSDatasourceClientBuilder, headers map[string]string) (expr.SQLSchemas, error) { + s := &ServiceImpl{ + log: log, + dataSourceCache: dscache, + expressionService: exprService, + dataSourceRequestValidator: validations.ProvideValidator(), + qsDatasourceClientBuilder: qsDatasourceClientBuilder, + headers: headers, + concurrentQueryLimit: 16, // TODO: make it configurable + } + + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + return s.GetSQLSchemas(ctx, user, reqDTO) +} diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index d69c69af134..138812ebd03 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -73,6 +73,8 @@ type Service interface { // this is more "forward compatible", for example supports per-query time ranges QueryDataNew(ctx context.Context, user identity.Requester, skipDSCache bool, reqDTO dtos.MetricRequest) (*backend.QueryDataResponse, error) + + GetSQLSchemas(ctx context.Context, user identity.Requester, reqDTO dtos.MetricRequest) (expr.SQLSchemas, error) } // Gives us compile time error if the service does not adhere to the contract of the interface diff --git a/pkg/services/query/query_service_mock.go b/pkg/services/query/query_service_mock.go index 22c08118941..c391b86061f 100644 --- a/pkg/services/query/query_service_mock.go +++ b/pkg/services/query/query_service_mock.go @@ -4,10 +4,12 @@ package query import ( context "context" + "fmt" backend "github.com/grafana/grafana-plugin-sdk-go/backend" dtos "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/expr" identity "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -97,6 +99,10 @@ func (_m *FakeQueryService) Run(ctx context.Context) error { return r0 } +func (_m *FakeQueryService) GetSQLSchemas(ctx context.Context, user identity.Requester, reqDTO dtos.MetricRequest) (expr.SQLSchemas, error) { + return nil, fmt.Errorf("sql schema endpoint not supported with public dashboards") +} + // NewFakeQueryService creates a new instance of FakeQueryService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFakeQueryService(t interface { diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index aead5e3cc00..02a4dea4824 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -844,7 +844,7 @@ func setup(t *testing.T, isMultiTenant bool, mockClient clientapi.QueryDataClien secretStore: ss, pluginRequestValidator: rv, queryService: queryService, - signedInUser: &user.SignedInUser{OrgID: 1, Login: "login", Name: "name", Email: "email", OrgRole: identity.RoleAdmin}, + signedInUser: &user.SignedInUser{OrgID: 1, Login: "login", Name: "name", Email: "email", OrgRole: identity.RoleAdmin, Namespace: "ns1"}, } } @@ -928,12 +928,15 @@ func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryData } type testClient struct { - queryDataLastCalledWith data.QueryDataRequest + queryDataLastCalledWith data.QueryDataRequest + // The number of times the QueryData method has been called + queryDataCalls int queryDataStubbedResponse *backend.QueryDataResponse queryDataStubbedError error } func (c *testClient) QueryData(ctx context.Context, req data.QueryDataRequest) (*backend.QueryDataResponse, error) { + c.queryDataCalls++ c.queryDataLastCalledWith = req if c.queryDataStubbedError != nil { return nil, c.queryDataStubbedError diff --git a/pkg/services/shorturls/shorturl.go b/pkg/services/shorturls/shorturl.go index d62f469f85c..99040011f50 100644 --- a/pkg/services/shorturls/shorturl.go +++ b/pkg/services/shorturls/shorturl.go @@ -4,12 +4,12 @@ import ( "context" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/apimachinery/identity" ) type Service interface { - GetShortURLByUID(ctx context.Context, user *user.SignedInUser, uid string) (*ShortUrl, error) - CreateShortURL(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*ShortUrl, error) + GetShortURLByUID(ctx context.Context, user identity.Requester, uid string) (*ShortUrl, error) + CreateShortURL(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*ShortUrl, error) UpdateLastSeenAt(ctx context.Context, shortURL *ShortUrl) error DeleteStaleShortURLs(ctx context.Context, cmd *DeleteShortUrlCommand) error ConvertShortURLToDTO(shortURL *ShortUrl, appURL string) *dtos.ShortURL diff --git a/pkg/services/shorturls/shorturlimpl/shorturl.go b/pkg/services/shorturls/shorturlimpl/shorturl.go index 51753b74c6e..a363a4d7627 100644 --- a/pkg/services/shorturls/shorturlimpl/shorturl.go +++ b/pkg/services/shorturls/shorturlimpl/shorturl.go @@ -8,9 +8,9 @@ import ( "time" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/shorturls" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" ) @@ -28,7 +28,7 @@ func ProvideService(db db.DB) *ShortURLService { } } -func (s ShortURLService) GetShortURLByUID(ctx context.Context, user *user.SignedInUser, uid string) (*shorturls.ShortUrl, error) { +func (s ShortURLService) GetShortURLByUID(ctx context.Context, user identity.Requester, uid string) (*shorturls.ShortUrl, error) { return s.SQLStore.Get(ctx, user, uid) } @@ -40,7 +40,7 @@ func (s ShortURLService) List(ctx context.Context, orgID int64) ([]*shorturls.Sh return s.SQLStore.List(ctx, orgID) } -func (s ShortURLService) CreateShortURL(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) { +func (s ShortURLService) CreateShortURL(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) { relPath := strings.TrimSpace(cmd.Path) if path.IsAbs(relPath) { @@ -74,12 +74,12 @@ func (s ShortURLService) CreateShortURL(ctx context.Context, user *user.SignedIn now := time.Now().Unix() shortURL := shorturls.ShortUrl{ - OrgId: user.OrgID, + OrgId: user.GetOrgID(), Uid: uid, Path: relPath, - CreatedBy: user.UserID, CreatedAt: now, } + shortURL.CreatedBy, _ = user.GetInternalID() if err := s.SQLStore.Insert(ctx, &shortURL); err != nil { return nil, shorturls.ErrShortURLInternal.Errorf("failed to insert shorturl: %w", err) diff --git a/pkg/services/shorturls/shorturlimpl/store.go b/pkg/services/shorturls/shorturlimpl/store.go index 6e0a072d23c..ad74b269d45 100644 --- a/pkg/services/shorturls/shorturlimpl/store.go +++ b/pkg/services/shorturls/shorturlimpl/store.go @@ -3,13 +3,13 @@ package shorturlimpl import ( "context" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/shorturls" - "github.com/grafana/grafana/pkg/services/user" ) type store interface { - Get(ctx context.Context, user *user.SignedInUser, uid string) (*shorturls.ShortUrl, error) + Get(ctx context.Context, user identity.Requester, uid string) (*shorturls.ShortUrl, error) Update(ctx context.Context, shortURL *shorturls.ShortUrl) error Insert(ctx context.Context, shortURL *shorturls.ShortUrl) error Delete(ctx context.Context, cmd *shorturls.DeleteShortUrlCommand) error @@ -20,10 +20,10 @@ type sqlStore struct { db db.DB } -func (s sqlStore) Get(ctx context.Context, user *user.SignedInUser, uid string) (*shorturls.ShortUrl, error) { +func (s sqlStore) Get(ctx context.Context, user identity.Requester, uid string) (*shorturls.ShortUrl, error) { var shortURL shorturls.ShortUrl err := s.db.WithDbSession(ctx, func(dbSession *db.Session) error { - exists, err := dbSession.Where("org_id=? AND uid=?", user.OrgID, uid).Get(&shortURL) + exists, err := dbSession.Where("org_id=? AND uid=?", user.GetOrgID(), uid).Get(&shortURL) if err != nil { return err } diff --git a/pkg/services/sqlstore/migrations/libraryelements.go b/pkg/services/sqlstore/migrations/libraryelements.go index 903411997ac..55395fa51a8 100644 --- a/pkg/services/sqlstore/migrations/libraryelements.go +++ b/pkg/services/sqlstore/migrations/libraryelements.go @@ -83,4 +83,12 @@ func addLibraryElementsMigrations(mg *migrator.Migrator) { mg.AddMigration("populate library_element folder_uid", migrator.NewRawSQLMigration(q)) mg.AddMigration("add index library_element org_id-folder_uid-name-kind", migrator.NewAddIndexMigration(libraryElementsV1, &migrator.Index{Cols: []string{"org_id", "folder_uid", "name", "kind"}, Type: migrator.UniqueIndex})) + + mg.AddMigration("drop unique name in folder index (id)", + migrator.NewDropIndexMigration(libraryElementsV1, + &migrator.Index{Cols: []string{"org_id", "folder_id", "name", "kind"}, Type: migrator.UniqueIndex})) + + mg.AddMigration("drop unique name in folder index", + migrator.NewDropIndexMigration(libraryElementsV1, + &migrator.Index{Cols: []string{"org_id", "folder_uid", "name", "kind"}, Type: migrator.UniqueIndex})) } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 04bcbc98f31..1762d98a075 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -156,4 +156,8 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) { ualert.DropTitleUniqueIndexMigration(mg) ualert.AddStateFiredAtColumn(mg) + + ualert.CollateAlertRuleGroup(mg) + + ualert.AddAlertRuleGroupIndexMigration(mg) } diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_group_collation.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_group_collation.go new file mode 100644 index 00000000000..5d7f7e3598a --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_group_collation.go @@ -0,0 +1,9 @@ +package ualert + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// CollateAlertRuleGroup ensures that rule_group column collates. +func CollateAlertRuleGroup(mg *migrator.Migrator) { + mg.AddMigration("ensure rule_group column is case sensitive in returned results", migrator.NewRawSQLMigration(""). + Mysql("ALTER TABLE alert_rule MODIFY rule_group VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs NOT NULL;")) +} diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_group_index.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_group_index.go new file mode 100644 index 00000000000..e7df19a324d --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_group_index.go @@ -0,0 +1,14 @@ +package ualert + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// AddAlertRuleGroupIndexMigration adds an index on org_id, namespace_uid, rule_group, and rule_group_idx columns to alert_rule table. +func AddAlertRuleGroupIndexMigration(mg *migrator.Migrator) { + mg.AddMigration("add index in alert_rule on org_id, namespace_uid, rule_group and rule_group_idx columns", migrator.NewAddIndexMigration( + migrator.Table{Name: "alert_rule"}, + &migrator.Index{ + Name: "IDX_alert_rule_org_id_namespace_uid_rule_group_rule_group_idx", + Cols: []string{"org_id", "namespace_uid", "rule_group", "rule_group_idx"}, + }, + )) +} diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index af9f0d235cd..46ef2442e7d 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -181,6 +181,18 @@ func addUserMigrations(mg *Migrator) { mg.AddMigration("Add index on user.is_service_account and user.last_seen_at", NewAddIndexMigration(userV2, &Index{ Cols: []string{"is_service_account", "last_seen_at"}, Type: IndexType, })) + + // Expand uid column to safely accommodate 'scim-' prefix without truncation/collisions + mg.AddMigration("Expand user.uid length to 190", NewRawSQLMigration(""). + SQLite("SELECT 1;"). + Postgres("ALTER TABLE `user` ALTER COLUMN uid TYPE VARCHAR(190);"). + Mysql("ALTER TABLE user MODIFY uid VARCHAR(190);")) + + // Prefix SCIM UID for provisioned users to avoid numeric/existing-id collisions + mg.AddMigration("Prefix SCIM uid for provisioned users", NewRawSQLMigration(""). + SQLite("UPDATE user SET uid = 'scim-' || uid WHERE is_provisioned = 1 AND uid NOT LIKE 'scim-%';"). + Postgres("UPDATE `user` SET uid = 'scim-' || uid WHERE is_provisioned = TRUE AND uid NOT LIKE 'scim-%';"). + Mysql("UPDATE user SET uid = CONCAT('scim-', uid) WHERE is_provisioned = 1 AND uid NOT LIKE 'scim-%';")) } const migSQLITEisServiceAccountNullable = `ALTER TABLE user ADD COLUMN tmp_service_account BOOLEAN DEFAULT 0; diff --git a/pkg/services/sqlstore/sqlstore_testinfra.go b/pkg/services/sqlstore/sqlstore_testinfra.go index c75dfc28645..9d05f72b3fb 100644 --- a/pkg/services/sqlstore/sqlstore_testinfra.go +++ b/pkg/services/sqlstore/sqlstore_testinfra.go @@ -195,7 +195,7 @@ func NewTestStore(tb TestingTB, opts ...TestOption) *SQLStore { tb.Fatalf("failed to truncate DB tables after migrations: %v", err) panic("unreachable") } - testSQLStore.engine.ResetSequenceGenerator() + store.engine.ResetSequenceGenerator() } return store diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index 68f655771b0..4d01c0a3d0b 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -38,6 +38,7 @@ type Team struct { // COMMANDS type CreateTeamCommand struct { + // required:true Name string `json:"name" binding:"Required"` Email string `json:"email"` ExternalUID string `json:"-"` @@ -94,14 +95,21 @@ type SearchTeamsQuery struct { } type TeamDTO struct { - ID int64 `json:"id" xorm:"id"` - UID string `json:"uid" xorm:"uid"` - OrgID int64 `json:"orgId" xorm:"org_id"` - Name string `json:"name"` - Email string `json:"email"` - ExternalUID string `json:"externalUID" xorm:"external_uid"` - IsProvisioned bool `json:"isProvisioned"` - AvatarURL string `json:"avatarUrl"` + // @deprecated Use UID instead + // required: true + ID int64 `json:"id" xorm:"id"` + // required: true + UID string `json:"uid" xorm:"uid"` + // required: true + OrgID int64 `json:"orgId" xorm:"org_id"` + // required: true + Name string `json:"name"` + Email string `json:"email"` + ExternalUID string `json:"externalUID" xorm:"external_uid"` + // required: true + IsProvisioned bool `json:"isProvisioned"` + AvatarURL string `json:"avatarUrl"` + // required: true MemberCount int64 `json:"memberCount"` Permission PermissionType `json:"permission"` AccessControl map[string]bool `json:"accessControl"` @@ -146,6 +154,7 @@ type TeamMember struct { // COMMANDS type AddTeamMemberCommand struct { + // required:true UserID int64 `json:"userId" binding:"Required"` Permission PermissionType `json:"-"` } diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index 69436edd8cc..9fca7089d2b 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -344,6 +344,13 @@ type SearchTeamsParams struct { // If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded. // required:false Query string `json:"query"` + // in:query + // required:false + // default: false + AccessControl bool `json:"accesscontrol"` + // in:query + // required:false + Sort string `json:"sort"` } // swagger:parameters createTeam diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 5cad5c1ba9b..c53feab2c76 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -215,6 +215,7 @@ type Cfg struct { ForwardHostEnvVars []string PreinstallPluginsAsync []InstallPlugin PreinstallPluginsSync []InstallPlugin + PreinstallAutoUpdate bool PluginsCDNURLTemplate string PluginLogBackendRequests bool @@ -389,14 +390,15 @@ type Cfg struct { LocalFileSystemAvailable bool // Analytics - CheckForGrafanaUpdates bool - CheckForPluginUpdates bool - ReportingDistributor string - ReportingEnabled bool - ApplicationInsightsConnectionString string - ApplicationInsightsEndpointUrl string - FeedbackLinksEnabled bool - ReportingStaticContext map[string]string + CheckForGrafanaUpdates bool + CheckForPluginUpdates bool + ReportingDistributor string + ReportingEnabled bool + ApplicationInsightsConnectionString string + ApplicationInsightsEndpointUrl string + ApplicationInsightsAutoRouteTracking bool + FeedbackLinksEnabled bool + ReportingStaticContext map[string]string // Frontend analytics GoogleAnalyticsID string @@ -1272,6 +1274,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.ApplicationInsightsConnectionString = analytics.Key("application_insights_connection_string").String() cfg.ApplicationInsightsEndpointUrl = analytics.Key("application_insights_endpoint_url").String() + cfg.ApplicationInsightsAutoRouteTracking = analytics.Key("application_insights_connection_string").MustBool(true) cfg.FeedbackLinksEnabled = analytics.Key("feedback_links_enabled").MustBool(true) // parse reporting static context string of key=value, key=value pairs into an object diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index 0080f61d0a4..a6842a276a0 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -185,6 +185,8 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { } cfg.PreinstallPluginsAsync = nil } + + cfg.PreinstallAutoUpdate = pluginsSection.Key("preinstall_auto_update").MustBool(true) } cfg.PluginCatalogURL = pluginsSection.Key("plugin_catalog_url").MustString("https://grafana.com/grafana/plugins/") diff --git a/pkg/setting/setting_secrets_manager.go b/pkg/setting/setting_secrets_manager.go index 926a0c4b962..d05e6f40f85 100644 --- a/pkg/setting/setting_secrets_manager.go +++ b/pkg/setting/setting_secrets_manager.go @@ -35,8 +35,13 @@ type SecretsManagerSettings struct { GCWorkerPollInterval time.Duration // How long to wait for the process to clean up a secure value to complete. GCWorkerPerSecureValueCleanupTimeout time.Duration - // Whether the secrets management is running in developer mode. - IsDeveloperMode bool + + // Whether to register the MT CRUD API + RegisterAPIServer bool + // Whether to create the MT secrets management database + RunSecretsDBMigrations bool + // Whether to run the data key id migration. Requires that RunSecretsDBMigrations is also true. + RunDataKeyMigration bool } func (cfg *Cfg) readSecretsManagerSettings() { @@ -57,7 +62,9 @@ func (cfg *Cfg) readSecretsManagerSettings() { cfg.SecretsManagement.GCWorkerPollInterval = secretsMgmt.Key("gc_worker_poll_interval").MustDuration(1 * time.Minute) cfg.SecretsManagement.GCWorkerPerSecureValueCleanupTimeout = secretsMgmt.Key("gc_worker_per_request_timeout").MustDuration(5 * time.Second) - cfg.SecretsManagement.IsDeveloperMode = secretsMgmt.Key("developer_mode").MustBool(false) + cfg.SecretsManagement.RegisterAPIServer = secretsMgmt.Key("register_api_server").MustBool(true) + cfg.SecretsManagement.RunSecretsDBMigrations = secretsMgmt.Key("run_secrets_db_migrations").MustBool(true) + cfg.SecretsManagement.RunDataKeyMigration = secretsMgmt.Key("run_data_key_migration").MustBool(true) // Extract available KMS providers from configuration sections providers := make(map[string]map[string]string) diff --git a/pkg/setting/setting_secrets_manager_test.go b/pkg/setting/setting_secrets_manager_test.go index a71a36e0155..c326c250821 100644 --- a/pkg/setting/setting_secrets_manager_test.go +++ b/pkg/setting/setting_secrets_manager_test.go @@ -171,18 +171,18 @@ domain = example.com assert.Empty(t, cfg.SecretsManagement.ConfiguredKMSProviders) }) - t.Run("should handle configuration with developer mode on", func(t *testing.T) { + t.Run("should handle configuration with register_api_server disabled", func(t *testing.T) { iniContent := ` [secrets_manager] -developer_mode = true +register_api_server = false ` cfg, err := NewCfgFromBytes([]byte(iniContent)) require.NoError(t, err) - assert.True(t, cfg.SecretsManagement.IsDeveloperMode) + assert.False(t, cfg.SecretsManagement.RegisterAPIServer) }) - t.Run("should handle configuration without developer mode set", func(t *testing.T) { + t.Run("should handle configuration without register_api_server set", func(t *testing.T) { iniContent := ` [secrets_manager] encryption_provider = aws_kms @@ -190,6 +190,50 @@ encryption_provider = aws_kms cfg, err := NewCfgFromBytes([]byte(iniContent)) require.NoError(t, err) - assert.False(t, cfg.SecretsManagement.IsDeveloperMode) + assert.True(t, cfg.SecretsManagement.RegisterAPIServer) + }) + + t.Run("should handle configuration with run_secrets_db_migrations disabled", func(t *testing.T) { + iniContent := ` +[secrets_manager] +run_secrets_db_migrations = false +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.False(t, cfg.SecretsManagement.RunSecretsDBMigrations) + }) + + t.Run("should handle configuration without run_secrets_db_migrations set", func(t *testing.T) { + iniContent := ` +[secrets_manager] +encryption_provider = aws_kms +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.True(t, cfg.SecretsManagement.RunSecretsDBMigrations) + }) + + t.Run("should handle configuration with run_data_key_migration disabled", func(t *testing.T) { + iniContent := ` +[secrets_manager] +run_data_key_migration = false +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.False(t, cfg.SecretsManagement.RunDataKeyMigration) + }) + + t.Run("should handle configuration without run_data_key_migration set", func(t *testing.T) { + iniContent := ` +[secrets_manager] +encryption_provider = aws_kms +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.True(t, cfg.SecretsManagement.RunDataKeyMigration) }) } diff --git a/pkg/setting/settings_zanzana.go b/pkg/setting/settings_zanzana.go index be7121f3b0d..1a06ea034ef 100644 --- a/pkg/setting/settings_zanzana.go +++ b/pkg/setting/settings_zanzana.go @@ -27,6 +27,8 @@ type ZanzanaClientSettings struct { // URL called to perform exchange request. // Only used when mode is set to client. TokenExchangeURL string + // Namespace to use for the token. + TokenNamespace string } type ZanzanaServerSettings struct { @@ -113,6 +115,10 @@ func (cfg *Cfg) readZanzanaSettings() { zc.Addr = clientSec.Key("address").MustString("") zc.ServerCertFile = clientSec.Key("tls_cert").MustString("") + // TODO: read Token and TokenExchangeURL from grpc_client_authentication section + grpcClientAuthSection := cfg.SectionWithEnvOverrides("grpc_client_authentication") + zc.TokenNamespace = grpcClientAuthSection.Key("token_namespace").MustString("stacks-" + cfg.StackID) + cfg.ZanzanaClient = zc zs := ZanzanaServerSettings{} diff --git a/pkg/storage/legacysql/dualwrite/dualwriter.go b/pkg/storage/legacysql/dualwrite/dualwriter.go index 72c05d5ed4d..ba4e757742e 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter.go @@ -5,6 +5,9 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" @@ -19,14 +22,17 @@ import ( ) var ( - _ grafanarest.Storage = (*dualWriter)(nil) + _ grafanarest.Storage = (*dualWriter)(nil) + tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/legacysql/dualwrite") ) -// Let's give the background queries a bit more time to complete -// as we also run them as part of load tests that might need longer -// to complete. Those run in the background and won't impact the -// user experience in any way. -const backgroundReqTimeout = time.Minute +const ( + // Let's give the background queries a bit more time to complete + // as we also run them as part of load tests that might need longer + // to complete. Those run in the background and won't impact the + // user experience in any way. + backgroundReqTimeout = time.Minute +) // dualWriter will write first to legacy, then to unified keeping the same internal ID type dualWriter struct { @@ -37,6 +43,13 @@ type dualWriter struct { } func (d *dualWriter) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := tracer.Start(ctx, "dualwrite.dualWriter.Get", + trace.WithAttributes( + attribute.Bool("errorIsOK", d.errorIsOK), + attribute.Bool("readUnified", d.readUnified))) + defer span.End() + + log := logging.FromContext(ctx).With("method", "Get", "name", name) // If we read from unified, we can just do that and return. if d.readUnified { return d.unified.Get(ctx, name, options) @@ -44,6 +57,7 @@ func (d *dualWriter) Get(ctx context.Context, name string, options *metav1.GetOp // If legacy is still our main store, lets first read from it. legacyGet, err := d.legacy.Get(ctx, name, options) if err != nil { + log.Error("failed to GET object from legacy storage", "err", err) return nil, err } // Once we have successfully read from legacy, we can check if we want to fail on a unified read. @@ -52,7 +66,6 @@ func (d *dualWriter) Get(ctx context.Context, name string, options *metav1.GetOp go func(ctxBg context.Context, cancel context.CancelFunc) { defer cancel() if _, err := d.unified.Get(ctxBg, name, options); err != nil { - log := logging.FromContext(ctxBg).With("method", "Get") log.Error("failed background GET to unified", "err", err) } }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) @@ -61,17 +74,24 @@ func (d *dualWriter) Get(ctx context.Context, name string, options *metav1.GetOp // If it's not okay to fail, we have to check it in the foreground. _, unifiedErr := d.unified.Get(ctx, name, options) if unifiedErr != nil && !apierrors.IsNotFound(unifiedErr) { + log.Error("failed to GET object from unified storage", "err", unifiedErr) return nil, unifiedErr } return legacyGet, nil } func (d *dualWriter) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { + ctx, span := tracer.Start(ctx, "dualwrite.dualWriter.List", + trace.WithAttributes( + attribute.Bool("errorIsOK", d.errorIsOK), + attribute.Bool("readUnified", d.readUnified))) + defer span.End() + // Always work on *copies* so we never mutate the caller's ListOptions. var ( legacyOptions = options.DeepCopy() unifiedOptions = options.DeepCopy() - log = logging.FromContext(ctx).With("method", "List") + log = logging.FromContext(ctx).With("method", "List", "options", options) ) legacyToken, unifiedToken, err := parseContinueTokens(options.Continue) @@ -86,6 +106,7 @@ func (d *dualWriter) List(ctx context.Context, options *metainternalversion.List if d.readUnified { unifiedList, err := d.unified.List(ctx, unifiedOptions) if err != nil { + log.Error("failed to list objects from unified storage", "err", err) return nil, err } unifiedMeta, err := meta.ListAccessor(unifiedList) @@ -115,6 +136,7 @@ func (d *dualWriter) List(ctx context.Context, options *metainternalversion.List // If legacy is still the main store, lets first read from it. legacyList, err := d.legacy.List(ctx, legacyOptions) if err != nil { + log.Error("failed to list objects from legacy storage", "err", err) return nil, err } legacyMeta, err := meta.ListAccessor(legacyList) @@ -158,6 +180,7 @@ func (d *dualWriter) List(ctx context.Context, options *metainternalversion.List // If it's not okay to fail, we have to check it in the foreground. unifiedList, err := d.unified.List(ctx, unifiedOptions) if err != nil { + log.Error("failed to list objects from unified storage", "err", err) return nil, err } unifiedMeta, err := meta.ListAccessor(unifiedList) @@ -172,6 +195,12 @@ func (d *dualWriter) List(ctx context.Context, options *metainternalversion.List // Create overrides the behavior of the generic DualWriter and writes to LegacyStorage and Storage. func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := tracer.Start(ctx, "dualwrite.dualWriter.Create", + trace.WithAttributes( + attribute.Bool("errorIsOK", d.errorIsOK), + attribute.Bool("readUnified", d.readUnified))) + defer span.End() + log := logging.FromContext(ctx).With("method", "Create") accIn, err := meta.Accessor(in) @@ -207,7 +236,7 @@ func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValida // will try to cleanup the object in legacy. createdFromLegacy, err := d.legacy.Create(ctx, in, createValidation, options) if err != nil { - log.Error("unable to create object in legacy storage", "err", err) + log.With("objectInfo", objectInfo(in)).Error("failed to CREATE object in legacy storage", "err", err) return nil, err } @@ -234,12 +263,14 @@ func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValida if d.readUnified { storageObj, errObjectSt := d.unified.Create(ctx, createdCopy, createValidation, options) if errObjectSt != nil { - log.Error("unable to create object in unified storage", "err", errObjectSt) + log.With("objectInfo", objectInfo(createdCopy)).Error("failed to CREATE object in unified storage", "err", errObjectSt) // If we cannot create in unified storage, attempt to clean up legacy. - _, _, err = d.legacy.Delete(ctx, accCreated.GetName(), nil, &metav1.DeleteOptions{}) - if err != nil { - log.Error("unable to cleanup object in legacy storage", "err", err) - } + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + if _, asyncDelete, err := d.legacy.Delete(ctxBg, accCreated.GetName(), nil, &metav1.DeleteOptions{}); err != nil { + log.With("name", accCreated.GetName()).Error("failed to CLEANUP object in legacy storage", "err", err, "asyncDelete", asyncDelete) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) return nil, errObjectSt } return storageObj, nil @@ -248,22 +279,23 @@ func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValida go func(ctxBg context.Context, cancel context.CancelFunc) { defer cancel() if _, err := d.unified.Create(ctxBg, createdCopy, createValidation, options); err != nil { - log.Error("unable to create object in unified storage", "err", err) + log.With("objectInfo", objectInfo(createdCopy)).Error("failed to CREATE object in unified storage", "err", err) } }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) } else { // Otherwise let's create it in the foreground and return any error. if _, err := d.unified.Create(ctx, createdCopy, createValidation, options); err != nil { - log.Error("unable to create object in unified storage", "err", err) + log.With("objectInfo", objectInfo(createdCopy)).Error("failed to CREATE object in unified storage", "err", err) if d.errorIsOK { return createdFromLegacy, nil } - // If we cannot create in unified storage, attempt to clean up legacy. - _, _, errLegacy := d.legacy.Delete(ctx, accCreated.GetName(), nil, &metav1.DeleteOptions{}) - if errLegacy != nil { - log.Error("unable to cleanup object in legacy storage", "err", errLegacy) - } + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + if _, asyncDelete, err := d.legacy.Delete(ctxBg, accCreated.GetName(), nil, &metav1.DeleteOptions{}); err != nil { + log.With("name", accCreated.GetName()).Error("failed to CLEANUP object in legacy storage", "err", err, "asyncDelete", asyncDelete) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) return nil, err } } @@ -281,10 +313,17 @@ func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation r // By setting RemovePermissions to false in the context, we will skip the deletion of permissions // in the legacy store. This is needed as otherwise the permissions would be missing when executing // the delete operation in the unified storage store. + ctx, span := tracer.Start(ctx, "dualwrite.dualWriter.Delete", + trace.WithAttributes( + attribute.Bool("errorIsOK", d.errorIsOK), + attribute.Bool("readUnified", d.readUnified))) + defer span.End() + log := logging.FromContext(ctx).With("method", "Delete", "name", name) ctx = utils.SetFolderRemovePermissions(ctx, false) objFromLegacy, asyncLegacy, err := d.legacy.Delete(ctx, name, deleteValidation, options) if err != nil && (!d.readUnified || !d.errorIsOK && !apierrors.IsNotFound(err)) { + log.Error("failed to DELETE object in legacy storage", "err", err) return nil, false, err } @@ -295,6 +334,7 @@ func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation r if d.readUnified { objFromStorage, asyncStorage, err := d.unified.Delete(ctx, name, deleteValidation, options) if err != nil && !apierrors.IsNotFound(err) && !d.errorIsOK { + log.Error("failed to DELETE object in unified storage", "err", err) return nil, false, err } return objFromStorage, asyncStorage, nil @@ -304,7 +344,6 @@ func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation r defer cancel() _, _, err := d.unified.Delete(ctxBg, name, deleteValidation, options) if err != nil && !apierrors.IsNotFound(err) && !d.errorIsOK { - log := logging.FromContext(ctxBg).With("method", "Delete") log.Error("failed background DELETE in unified storage", "err", err) } }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) @@ -312,6 +351,7 @@ func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation r // Otherwise we just run it in the foreground and return an error if any might happen. _, _, err = d.unified.Delete(ctx, name, deleteValidation, options) if err != nil && !apierrors.IsNotFound(err) && !d.errorIsOK { + log.Error("failed to DELETE object in unified storage", "err", err) return nil, false, err } return objFromLegacy, asyncLegacy, nil @@ -319,8 +359,12 @@ func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation r // Update overrides the behavior of the generic DualWriter and writes first to Storage and then to LegacyStorage. func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - log := logging.FromContext(ctx).With("method", "Update") - + ctx, span := tracer.Start(ctx, "dualwrite.dualWriter.Update", + trace.WithAttributes( + attribute.Bool("errorIsOK", d.errorIsOK), + attribute.Bool("readUnified", d.readUnified))) + defer span.End() + log := logging.FromContext(ctx).With("method", "Update", "name", name) // update in legacy first, and then unistore. Will return a failure if either fails. // // we want to update in legacy first, otherwise if the update from unistore was successful, @@ -341,7 +385,7 @@ func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.Updat objFromLegacy, createdLegacy, err := d.legacy.Update(ctx, name, legacyInfo, createValidation, updateValidation, legacyForceCreate, options) if err != nil { - log.With("object", objFromLegacy).Error("could not update in legacy storage", "err", err) + log.Error("failed to UPDATE in legacy storage", "err", err) return nil, false, err } @@ -350,7 +394,7 @@ func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.Updat if createdLegacy { legacyMeta, err := utils.MetaAccessor(objFromLegacy) if err != nil { - log.With("object", objFromLegacy).Error("could not get meta accessor for legacy object", "err", err) + log.Error("failed to get meta accessor for legacy object", "err", err) return nil, false, err } unifiedInfo = &wrappedUpdateInfo{ @@ -367,13 +411,23 @@ func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.Updat go func(ctxBg context.Context, cancel context.CancelFunc) { defer cancel() if _, _, err := d.unified.Update(ctxBg, name, unifiedInfo, createValidation, updateValidation, unifiedForceCreate, options); err != nil { - log.Error("failed background UPDATE to unified storage", "err", err) + log.With("objectInfo", objectInfo(objFromLegacy)).Error("failed background UPDATE to unified storage", "err", err) } }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) return objFromLegacy, createdLegacy, nil } // If we want to check unified errors just run it in foreground. if _, _, err := d.unified.Update(ctx, name, unifiedInfo, createValidation, updateValidation, unifiedForceCreate, options); err != nil { + log.With("objectInfo", objectInfo(objFromLegacy)).Error("failed to UPDATE in unified storage", "err", err) + // cleanup the legacy object if we created it there + if createdLegacy { + go func(ctxBg context.Context, cancel context.CancelFunc) { + defer cancel() + if _, asyncDelete, err := d.legacy.Delete(ctxBg, name, nil, &metav1.DeleteOptions{}); err != nil { + log.With("name", name).Error("failed to CLEANUP object in legacy storage after unified storage update failure", "err", err, "asyncDelete", asyncDelete) + } + }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) + } return nil, false, err } return objFromLegacy, createdLegacy, nil @@ -381,6 +435,12 @@ func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.Updat // DeleteCollection overrides the behavior of the generic DualWriter and deletes from both LegacyStorage and Storage. func (d *dualWriter) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { + ctx, span := tracer.Start(ctx, "dualwrite.dualWriter.DeleteCollection", + trace.WithAttributes( + attribute.Bool("errorIsOK", d.errorIsOK), + attribute.Bool("readUnified", d.readUnified))) + defer span.End() + log := logging.FromContext(ctx).With("method", "DeleteCollection", "resourceVersion", listOptions.ResourceVersion) // delete from legacy first, and anything that is successful can be deleted in unistore too. @@ -391,7 +451,7 @@ func (d *dualWriter) DeleteCollection(ctx context.Context, deleteValidation rest deletedLegacy, err := d.legacy.DeleteCollection(ctx, deleteValidation, options, listOptions) if err != nil { - log.With("deleted", deletedLegacy).Error("failed to delete collection successfully from legacy storage", "err", err) + log.With("options", options).Error("failed to DELETE collection successfully from legacy storage", "err", err) return nil, err } @@ -403,14 +463,14 @@ func (d *dualWriter) DeleteCollection(ctx context.Context, deleteValidation rest go func(ctxBg context.Context, cancel context.CancelFunc) { defer cancel() if _, err := d.unified.DeleteCollection(ctxBg, deleteValidation, options, listOptions); err != nil { - log.Error("failed background DELETE collection to unified storage", "err", err) + log.With("objectInfo", objectInfo(deletedLegacy)).Error("failed background DELETE collection to unified storage", "err", err) } }(context.WithTimeout(context.WithoutCancel(ctx), backgroundReqTimeout)) return deletedLegacy, nil } // Otherwise we have to check the error and run it in the foreground. - if deletedStorage, err := d.unified.DeleteCollection(ctx, deleteValidation, options, listOptions); err != nil { - log.With("deleted", deletedStorage).Error("failed to delete collection successfully from Storage", "err", err) + if _, err := d.unified.DeleteCollection(ctx, deleteValidation, options, listOptions); err != nil { + log.With("objectInfo", objectInfo(deletedLegacy)).Error("failed to DELETE collection successfully from Storage", "err", err) return nil, err } return deletedLegacy, nil @@ -489,3 +549,30 @@ func (w *wrappedUpdateInfo) UpdatedObject(ctx context.Context, oldObj runtime.Ob meta.SetUID("") return obj, err } + +func objectInfo(obj runtime.Object) map[string]interface{} { + if obj == nil { + return map[string]interface{}{"object": "nil"} + } + + acc, err := meta.Accessor(obj) + if err != nil { + return map[string]interface{}{"object": fmt.Sprintf("%T", obj), "error": err.Error()} + } + + info := map[string]interface{}{ + "name": acc.GetName(), + } + + if ns := acc.GetNamespace(); ns != "" { + info["namespace"] = ns + } + if uid := acc.GetUID(); uid != "" { + info["uid"] = string(uid) + } + if rv := acc.GetResourceVersion(); rv != "" { + info["resourceVersion"] = rv + } + + return info +} diff --git a/pkg/storage/secret/encryption/data/encrypted_value_create.sql b/pkg/storage/secret/encryption/data/encrypted_value_create.sql index 7bce6a19bd4..7aa749e01a3 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_create.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_create.sql @@ -3,6 +3,7 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} ( {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} ( {{ .Arg .Row.Name }}, {{ .Arg .Row.Version }}, {{ .Arg .Row.EncryptedData }}, + {{ .Arg .Row.DataKeyID }}, {{ .Arg .Row.Created }}, {{ .Arg .Row.Updated }} ); diff --git a/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql b/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql index d318517346d..2a0446a3a9d 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_list_all.sql @@ -3,6 +3,7 @@ SELECT {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} FROM diff --git a/pkg/storage/secret/encryption/data/encrypted_value_read.sql b/pkg/storage/secret/encryption/data/encrypted_value_read.sql index 6a672554efb..3ff20a641f0 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_read.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_read.sql @@ -3,6 +3,7 @@ SELECT {{ .Ident "name" }}, {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, + {{ .Ident "data_key_id" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} FROM diff --git a/pkg/storage/secret/encryption/data/encrypted_value_update.sql b/pkg/storage/secret/encryption/data/encrypted_value_update.sql index 08e985297a1..c939c092c1c 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_update.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_update.sql @@ -2,6 +2,7 @@ UPDATE {{ .Ident "secret_encrypted_value" }} SET {{ .Ident "encrypted_data" }} = {{ .Arg .EncryptedData }}, + {{ .Ident "data_key_id" }} = {{ .Arg .DataKeyID }}, {{ .Ident "updated" }} = {{ .Arg .Updated }} WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND diff --git a/pkg/storage/secret/encryption/encrypted_value_model.go b/pkg/storage/secret/encryption/encrypted_value_model.go index eb66247c021..3c4ba7be905 100644 --- a/pkg/storage/secret/encryption/encrypted_value_model.go +++ b/pkg/storage/secret/encryption/encrypted_value_model.go @@ -6,6 +6,7 @@ type EncryptedValue struct { Namespace string Name string Version int64 + DataKeyID string EncryptedData []byte Created int64 Updated int64 diff --git a/pkg/storage/secret/encryption/encrypted_value_store.go b/pkg/storage/secret/encryption/encrypted_value_store.go index 3a10d01b957..af48a651868 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store.go +++ b/pkg/storage/secret/encryption/encrypted_value_store.go @@ -1,7 +1,9 @@ package encryption import ( + "bytes" "context" + "encoding/base64" "errors" "fmt" "time" @@ -10,6 +12,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) @@ -37,9 +40,9 @@ type encryptedValStorage struct { tracer trace.Tracer } -func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (ev *contracts.EncryptedValue, err error) { +func (s *encryptedValStorage) Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) (ev *contracts.EncryptedValue, err error) { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), )) defer span.End() @@ -56,10 +59,11 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string createdTime := time.Now().Unix() encryptedValue := &EncryptedValue{ - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, - EncryptedData: encryptedData, + EncryptedData: encryptedData.EncryptedData, + DataKeyID: encryptedData.DataKeyID, Created: createdTime, Updated: createdTime, } @@ -88,18 +92,21 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string } return &contracts.EncryptedValue{ - Namespace: encryptedValue.Namespace, - Name: encryptedValue.Name, - Version: encryptedValue.Version, - EncryptedData: encryptedValue.EncryptedData, - Created: encryptedValue.Created, - Updated: encryptedValue.Updated, + Namespace: encryptedValue.Namespace, + Name: encryptedValue.Name, + Version: encryptedValue.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: encryptedValue.DataKeyID, + EncryptedData: encryptedValue.EncryptedData, + }, + Created: encryptedValue.Created, + Updated: encryptedValue.Updated, }, nil } -func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error { +func (s *encryptedValStorage) Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) error { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -107,10 +114,11 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string req := updateEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, - EncryptedData: encryptedData, + EncryptedData: encryptedData.EncryptedData, + DataKeyID: encryptedData.DataKeyID, Updated: time.Now().Unix(), } @@ -133,9 +141,9 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string return nil } -func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, version int64) (*contracts.EncryptedValue, error) { +func (s *encryptedValStorage) Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*contracts.EncryptedValue, error) { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -143,7 +151,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v req := &readEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, } @@ -163,7 +171,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v } var encryptedValue EncryptedValue - err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated) + err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.DataKeyID, &encryptedValue.Created, &encryptedValue.Updated) if err != nil { return nil, fmt.Errorf("failed to scan encrypted value row: %w", err) } @@ -172,18 +180,21 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v } return &contracts.EncryptedValue{ - Namespace: encryptedValue.Namespace, - Name: encryptedValue.Name, - Version: encryptedValue.Version, - EncryptedData: encryptedValue.EncryptedData, - Created: encryptedValue.Created, - Updated: encryptedValue.Updated, + Namespace: encryptedValue.Namespace, + Name: encryptedValue.Name, + Version: encryptedValue.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: encryptedValue.DataKeyID, + EncryptedData: encryptedValue.EncryptedData, + }, + Created: encryptedValue.Created, + Updated: encryptedValue.Updated, }, nil } -func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string, version int64) error { +func (s *encryptedValStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes( - attribute.String("namespace", namespace), + attribute.String("namespace", namespace.String()), attribute.String("name", name), attribute.Int64("version", version), )) @@ -191,7 +202,7 @@ func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string req := deleteEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace, + Namespace: namespace.String(), Name: name, Version: version, } @@ -264,6 +275,7 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts. &row.Name, &row.Version, &row.EncryptedData, + &row.DataKeyID, &row.Created, &row.Updated, ) @@ -272,12 +284,15 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts. } encryptedValues = append(encryptedValues, &contracts.EncryptedValue{ - Namespace: row.Namespace, - Name: row.Name, - Version: row.Version, - EncryptedData: row.EncryptedData, - Created: row.Created, - Updated: row.Updated, + Namespace: row.Namespace, + Name: row.Name, + Version: row.Version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: row.DataKeyID, + EncryptedData: row.EncryptedData, + }, + Created: row.Created, + Updated: row.Updated, }) } if err := rows.Err(); err != nil { @@ -329,3 +344,77 @@ func (s *globalEncryptedValStorage) CountAll(ctx context.Context, untilTime *int return count, nil } + +type encryptedValMigrationExecutor struct { + db contracts.Database + dialect sqltemplate.Dialect + tracer trace.Tracer + encryptedValueStore contracts.EncryptedValueStorage + globalStore contracts.GlobalEncryptedValueStorage +} + +func ProvideEncryptedValueMigrationExecutor( + db contracts.Database, + tracer trace.Tracer, + encryptedValueStore contracts.EncryptedValueStorage, + globalStore contracts.GlobalEncryptedValueStorage, +) (contracts.EncryptedValueMigrationExecutor, error) { + return &encryptedValMigrationExecutor{ + db: db, + dialect: sqltemplate.DialectForDriver(db.DriverName()), + tracer: tracer, + encryptedValueStore: encryptedValueStore, + globalStore: globalStore, + }, nil +} + +func (s *encryptedValMigrationExecutor) Execute(ctx context.Context) (int, error) { + ctx, span := s.tracer.Start(ctx, "EncryptedValueMigrationExecutor.Execute") + defer span.End() + + // 1. Retrieve all encrypted values + encryptedValues, err := s.globalStore.ListAll(ctx, contracts.ListOpts{}, nil) + if err != nil { + return 0, fmt.Errorf("listing all encrypted values: %w", err) + } + + // This doesn't need to be done in a single transaction because there's no risk to successful rows if other rows fail + rowsAffected := 0 + for _, encryptedValue := range encryptedValues { + // 2. If the value already has the data key id broken out, skip it + if encryptedValue.DataKeyID != "" { + continue + } + + // 3. Split the data key id and the encrypted data out from the encoded payload + payload := encryptedValue.EncryptedData + const keyIdDelimiter = '#' + payload = payload[1:] + endOfKey := bytes.Index(payload, []byte{keyIdDelimiter}) + if endOfKey == -1 { + return rowsAffected, fmt.Errorf("could not find valid key id in encrypted payload with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version) + } + b64Key := payload[:endOfKey] + encryptedData := payload[endOfKey+1:] + if len(encryptedData) == 0 { + return rowsAffected, fmt.Errorf("encrypted data is empty with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version) + } + keyId := make([]byte, base64.RawStdEncoding.DecodedLen(len(b64Key))) + _, err := base64.RawStdEncoding.Decode(keyId, b64Key) + if err != nil { + return rowsAffected, fmt.Errorf("decoding key id with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err) + } + + // 4. Update the encrypted value with the data key id and the encrypted data + err = s.encryptedValueStore.Update(ctx, xkube.Namespace(encryptedValue.Namespace), encryptedValue.Name, encryptedValue.Version, contracts.EncryptedPayload{ + DataKeyID: string(keyId), + EncryptedData: encryptedData, + }) + if err != nil { + return rowsAffected, fmt.Errorf("updating encrypted value with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err) + } + rowsAffected++ + } + + return rowsAffected, nil +} diff --git a/pkg/storage/secret/encryption/encrypted_value_store_test.go b/pkg/storage/secret/encryption/encrypted_value_store_test.go index 620a034f22c..2f84e1f8575 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store_test.go +++ b/pkg/storage/secret/encryption/encrypted_value_store_test.go @@ -2,15 +2,25 @@ package encryption_test import ( "bytes" + "context" + "encoding/base64" "errors" + "fmt" "slices" "testing" + "text/template" "time" + "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" + cipherService "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" "pgregory.net/rapid" ) @@ -21,7 +31,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) require.NotEmpty(t, createdEV.Namespace) require.NotEmpty(t, createdEV.Name) @@ -36,10 +49,13 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) require.Equal(t, createdEV.Namespace, obtainedEV.Namespace) @@ -47,6 +63,7 @@ func TestEncryptedValueStoreImpl(t *testing.T) { require.Equal(t, createdEV.Created, obtainedEV.Created) require.Equal(t, createdEV.Updated, obtainedEV.Updated) require.Equal(t, createdEV.EncryptedData, obtainedEV.EncryptedData) + require.Equal(t, createdEV.DataKeyID, obtainedEV.DataKeyID) require.Equal(t, createdEV.Namespace, obtainedEV.Namespace) }) @@ -54,7 +71,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), "ns2", createdEV.Name, createdEV.Version) @@ -78,16 +98,23 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - err = sut.EncryptedValueStorage.Update(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version, []byte("test-data-updated")) + err = sut.EncryptedValueStorage.Update(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id-updated", + EncryptedData: []byte("test-data-updated"), + }) require.NoError(t, err) - updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) require.Equal(t, []byte("test-data-updated"), updatedEV.EncryptedData) + require.Equal(t, "test-data-key-id-updated", updatedEV.DataKeyID) require.Equal(t, createdEV.Created, updatedEV.Created) require.Equal(t, createdEV.Namespace, updatedEV.Namespace) }) @@ -96,7 +123,10 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, []byte("test-data")) + err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.Error(t, err) }) @@ -104,16 +134,19 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("ttttest-data")) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("ttttest-data"), + }) require.NoError(t, err) - _, err = sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + _, err = sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) - err = sut.EncryptedValueStorage.Delete(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + err = sut.EncryptedValueStorage.Delete(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.NoError(t, err) - obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(createdEV.Namespace), createdEV.Name, createdEV.Version) require.Error(t, err) require.Nil(t, obtainedEV) }) @@ -130,10 +163,16 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - createdEvA, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, []byte("test-data")) + createdEvA, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - createdEvB, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, []byte("test-data")) + createdEvB, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) // List all encrypted values, without pagination @@ -180,10 +219,16 @@ func TestEncryptedValueStoreImpl(t *testing.T) { t.Parallel() sut := testutils.Setup(t) - _, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, []byte("test-data")) + _, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-a", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) - _, err = sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, []byte("test-data")) + _, err = sut.EncryptedValueStorage.Create(t.Context(), "test-namespace-b", "test-name", 1, contracts.EncryptedPayload{ + DataKeyID: "test-data-key-id", + EncryptedData: []byte("test-data"), + }) require.NoError(t, err) count, err := sut.GlobalEncryptedValueStorage.CountAll(t.Context(), nil) @@ -198,6 +243,281 @@ func TestEncryptedValueStoreImpl(t *testing.T) { }) } +func TestEncryptedValueMigration(t *testing.T) { + t.Parallel() + + t.Run("golden path - successful migration of legacy format", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + tracer := noop.NewTracerProvider().Tracer("test") + usageStats := &usagestats.UsageStatsMock{T: t} + enc, err := cipherService.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + testCases := []struct { + namespace string + name string + version int64 + plaintext string + dataKeyId string + }{ + { + namespace: "test-namespace-1", + name: "test-name-1", + version: 1, + plaintext: "test-plaintext-1", + dataKeyId: "test-data-key-id-1", + }, + { + namespace: "test-namespace-1", + name: "test-name-2", + version: 1, + plaintext: "test-plaintext-2", + dataKeyId: "test-data-key-id-1", + }, + { + namespace: "test-namespace-2", + name: "test-name-3", + version: 1, + plaintext: "test-plaintext-3", + dataKeyId: "test-data-key-id-2", + }, + } + + // Seed with data in the legacy format + for _, tc := range testCases { + err := createLegacyEncryptedData(t, sut, enc, tc.namespace, tc.name, tc.version, tc.plaintext, tc.dataKeyId) + require.NoError(t, err) + } + + // Run the migration and blindy trust it + rowsAffected, err := sut.EncryptedValueMigrationExecutor.Execute(t.Context()) + require.NoError(t, err) + require.Equal(t, len(testCases), rowsAffected) + + // Now validate that the data is in the new format + encryptedValues, err := sut.GlobalEncryptedValueStorage.ListAll(t.Context(), contracts.ListOpts{}, nil) + require.NoError(t, err) + require.Len(t, encryptedValues, 3) + + for _, tc := range testCases { + ev, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(tc.namespace), tc.name, tc.version) + require.NoError(t, err) + + // Decrypt the encrypted data and check for equality + decrypted, err := enc.Decrypt(t.Context(), ev.EncryptedData, tc.dataKeyId) + require.NoError(t, err) + require.Equal(t, tc.dataKeyId, ev.DataKeyID) + require.Equal(t, tc.plaintext, string(decrypted)) + } + }) + + t.Run("error conditions - handles corrupt data gracefully", func(t *testing.T) { + t.Parallel() + + tracer := noop.NewTracerProvider().Tracer("test") + sut := testutils.Setup(t) + + t.Run("global store list error", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + listAllError: errors.New("database connection failed"), + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "listing all encrypted values") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - missing key delimiter", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("corrupt-data-without-delimiter"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "could not find valid key id in encrypted payload") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - empty encrypted data", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#dGVzdA#"), // Valid key but no encrypted data after delimiter + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "encrypted data is empty") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("corrupt data - invalid base64 key", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "test-ns", + Name: "test-name", + Version: 1, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#invalid-base64!@#$%^&*()#somedata"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "decoding key id") + require.Equal(t, 0, rowsAffected) + }) + + t.Run("update failure", func(t *testing.T) { + mockGlobalStore := &mockGlobalEncryptedValueStorage{ + encryptedValues: []*contracts.EncryptedValue{ + { + Namespace: "nonexistent-ns", + Name: "nonexistent-name", + Version: 999, + EncryptedPayload: contracts.EncryptedPayload{ + EncryptedData: []byte("#dGVzdA#someencrypteddata"), + DataKeyID: "", // Empty to trigger migration + }, + }, + }, + } + + migrationExecutor, err := encryption.ProvideEncryptedValueMigrationExecutor( + sut.Database, + tracer, + sut.EncryptedValueStorage, + mockGlobalStore, + ) + require.NoError(t, err) + + rowsAffected, err := migrationExecutor.Execute(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "updating encrypted value") + require.Equal(t, 0, rowsAffected) + }) + }) +} + +// Helper function that bypasses interfaces and creates data in the legacy format directly in the database. +// The format is "#{encoded_key_id}#{encrypted_data}". +func createLegacyEncryptedData(t *testing.T, sut testutils.Sut, enc cipher.Cipher, namespace, name string, version int64, plaintext string, dataKeyId string) error { + t.Helper() + + encryptedData, err := enc.Encrypt(t.Context(), []byte(plaintext), dataKeyId) + require.NoError(t, err) + + // Encode using the legacy format + const keyIdDelimiter = '#' + prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len(dataKeyId))+2) + base64.RawStdEncoding.Encode(prefix[1:], []byte(dataKeyId)) + prefix[0] = keyIdDelimiter + prefix[len(prefix)-1] = keyIdDelimiter + + blob := make([]byte, len(prefix)+len(encryptedData)) + copy(blob, prefix) + copy(blob[len(prefix):], encryptedData) + + createdTime := time.Now().Unix() + + encryptedValue := &encryption.EncryptedValue{ + Namespace: namespace, + Name: name, + Version: version, + EncryptedData: blob, + DataKeyID: "", + Created: createdTime, + Updated: createdTime, + } + + req := struct { + sqltemplate.SQLTemplate + Row *encryption.EncryptedValue + }{ + SQLTemplate: sqltemplate.New(sqltemplate.DialectForDriver(sut.Database.DriverName())), + Row: encryptedValue, + } + tmpl, err := template.ParseFiles("data/encrypted_value_create.sql") + if err != nil { + return fmt.Errorf("parsing template: %w", err) + } + + query, err := sqltemplate.Execute(tmpl, req) + if err != nil { + return fmt.Errorf("executing template: %w", err) + } + + res, err := sut.Database.ExecContext(t.Context(), query, req.GetArgs()...) + if err != nil { + return fmt.Errorf("inserting row: %w", err) + } + + if rowsAffected, err := res.RowsAffected(); err != nil { + return fmt.Errorf("getting rows affected: %w", err) + } else if rowsAffected != 1 { + return fmt.Errorf("expected 1 row affected, got %d", rowsAffected) + } + + return nil +} + func TestStateMachine(t *testing.T) { t.Parallel() @@ -212,10 +532,14 @@ func TestStateMachine(t *testing.T) { ns := namespaceGen.Draw(t, "ns") name := nameGen.Draw(t, "name") version := versionGen.Draw(t, "version") + dataKeyId := rapid.String().Draw(t, "dataKeyId") plaintext := rapid.String().Draw(t, "plaintext") - _, modelErr := m.create(ns, name, version, []byte(plaintext)) - _, err := sut.EncryptedValueStorage.Create(t.Context(), ns, name, version, []byte(plaintext)) + _, modelErr := m.create(ns, name, version, []byte(plaintext), dataKeyId) + _, err := sut.EncryptedValueStorage.Create(t.Context(), xkube.Namespace(ns), name, version, contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: []byte(plaintext), + }) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -225,10 +549,14 @@ func TestStateMachine(t *testing.T) { ns := namespaceGen.Draw(t, "ns") name := nameGen.Draw(t, "name") version := versionGen.Draw(t, "version") + dataKeyId := rapid.String().Draw(t, "dataKeyId") plaintext := rapid.String().Draw(t, "plaintext") - modelErr := m.update(ns, name, version, []byte(plaintext)) - err := sut.EncryptedValueStorage.Update(t.Context(), ns, name, version, []byte(plaintext)) + modelErr := m.update(ns, name, version, []byte(plaintext), dataKeyId) + err := sut.EncryptedValueStorage.Update(t.Context(), xkube.Namespace(ns), name, version, contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: []byte(plaintext), + }) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -240,7 +568,7 @@ func TestStateMachine(t *testing.T) { version := versionGen.Draw(t, "version") modelValue, modelErr := m.get(ns, name, version) - value, err := sut.EncryptedValueStorage.Get(t.Context(), ns, name, version) + value, err := sut.EncryptedValueStorage.Get(t.Context(), xkube.Namespace(ns), name, version) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -258,7 +586,7 @@ func TestStateMachine(t *testing.T) { version := versionGen.Draw(t, "version") modelErr := m.delete(ns, name, version) - err := sut.EncryptedValueStorage.Delete(t.Context(), ns, name, version) + err := sut.EncryptedValueStorage.Delete(t.Context(), xkube.Namespace(ns), name, version) if modelErr != nil || err != nil { require.ErrorIs(t, err, modelErr) return @@ -290,18 +618,19 @@ type entry struct { name string version int64 encryptedData []byte + dataKeyId string } func newModel() *model { return &model{} } -func (m *model) create(namespace, name string, version int64, encryptedData []byte) (*contracts.EncryptedValue, error) { +func (m *model) create(namespace, name string, version int64, encryptedData []byte, dataKeyId string) (*contracts.EncryptedValue, error) { v, err := m.get(namespace, name, version) if err != nil && !errors.Is(err, encryption.ErrEncryptedValueNotFound) { return nil, err } - // The entry being creted already exists + // The entry being created already exists if v != nil { return nil, encryption.ErrEncryptedValueAlreadyExists } @@ -311,20 +640,25 @@ func (m *model) create(namespace, name string, version int64, encryptedData []by name: name, version: version, encryptedData: encryptedData, + dataKeyId: dataKeyId, }) return &contracts.EncryptedValue{ - Namespace: namespace, - Name: name, - Version: version, - EncryptedData: encryptedData, - Created: 1, - Updated: 1, + Namespace: namespace, + Name: name, + Version: version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: dataKeyId, + EncryptedData: encryptedData, + }, + Created: 1, + Updated: 1, }, nil } -func (m *model) update(namespace, name string, version int64, encryptedData []byte) error { +func (m *model) update(namespace, name string, version int64, encryptedData []byte, dataKeyId string) error { for _, v := range m.entries { if v.namespace == namespace && v.name == name && v.version == version { v.encryptedData = encryptedData + v.dataKeyId = dataKeyId return nil } } @@ -336,12 +670,15 @@ func (m *model) get(namespace, name string, version int64) (*contracts.Encrypted for _, v := range m.entries { if v.namespace == namespace && v.name == name && v.version == version { return &contracts.EncryptedValue{ - Namespace: namespace, - Name: name, - Version: version, - EncryptedData: v.encryptedData, - Created: 1, - Updated: 1, + Namespace: namespace, + Name: name, + Version: version, + EncryptedPayload: contracts.EncryptedPayload{ + DataKeyID: v.dataKeyId, + EncryptedData: v.encryptedData, + }, + Created: 1, + Updated: 1, }, nil } } @@ -354,3 +691,25 @@ func (m *model) delete(namespace, name string, version int64) error { }) return nil } + +// mockGlobalEncryptedValueStorage is a mock implementation of contracts.GlobalEncryptedValueStorage +// used for testing error conditions in the migration executor +type mockGlobalEncryptedValueStorage struct { + encryptedValues []*contracts.EncryptedValue + listAllError error + countAllError error +} + +func (m *mockGlobalEncryptedValueStorage) ListAll(ctx context.Context, opts contracts.ListOpts, untilTime *int64) ([]*contracts.EncryptedValue, error) { + if m.listAllError != nil { + return nil, m.listAllError + } + return m.encryptedValues, nil +} + +func (m *mockGlobalEncryptedValueStorage) CountAll(ctx context.Context, untilTime *int64) (int64, error) { + if m.countAllError != nil { + return 0, m.countAllError + } + return int64(len(m.encryptedValues)), nil +} diff --git a/pkg/storage/secret/encryption/query.go b/pkg/storage/secret/encryption/query.go index 47ea83ce0cd..21e6081a7c7 100644 --- a/pkg/storage/secret/encryption/query.go +++ b/pkg/storage/secret/encryption/query.go @@ -74,6 +74,7 @@ type updateEncryptedValue struct { Name string Version int64 EncryptedData []byte + DataKeyID string Updated int64 } diff --git a/pkg/storage/secret/encryption/query_test.go b/pkg/storage/secret/encryption/query_test.go index a93ff0b77e6..4cbb830f0af 100644 --- a/pkg/storage/secret/encryption/query_test.go +++ b/pkg/storage/secret/encryption/query_test.go @@ -24,6 +24,7 @@ func TestEncryptedValueQueries(t *testing.T) { Name: "n1", Version: 1, EncryptedData: []byte("secret"), + DataKeyID: "test-data-key-id", Created: 1234, Updated: 5678, }, @@ -50,6 +51,7 @@ func TestEncryptedValueQueries(t *testing.T) { Name: "n1", Version: 1, EncryptedData: []byte("secret"), + DataKeyID: "test-data-key-id", Updated: 5679, }, }, diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql index 7a886b1c6ee..952c04730a8 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO `secret_encrypted_value` ( `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO `secret_encrypted_value` ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql index 74b699d45f1..e41d58588d4 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql index b34496aaf93..c51a40a9a5a 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql index 9c33fd6bdbf..de82acacbbd 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql index d7066395a78..f21f7122646 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql index bb9faddb8ff..bca020a26d3 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT `name`, `version`, `encrypted_data`, + `data_key_id`, `created`, `updated` FROM diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql index cf3d6897a64..b454cb5165a 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE `secret_encrypted_value` SET `encrypted_data` = '[115 101 99 114 101 116]', + `data_key_id` = 'test-data-key-id', `updated` = 5679 WHERE `namespace` = 'ns' AND diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql index 23d9a0f2331..17576f07232 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO "secret_encrypted_value" ( "name", "version", "encrypted_data", + "data_key_id", "created", "updated" ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO "secret_encrypted_value" ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql index 74432ebbf69..ee8b9c0a399 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql index 1d7089f751e..661d7f9ea0a 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql index 6f2bbd0b90f..06782d7e40d 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql index b9f326c8bf0..a64d45afaec 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql index 4bbe1000ce6..437685ae4e7 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql index b2b5e30ecac..0f710f7456d 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE "secret_encrypted_value" SET "encrypted_data" = '[115 101 99 114 101 116]', + "data_key_id" = 'test-data-key-id', "updated" = 5679 WHERE "namespace" = 'ns' AND diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql index 23d9a0f2331..17576f07232 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql @@ -3,6 +3,7 @@ INSERT INTO "secret_encrypted_value" ( "name", "version", "encrypted_data", + "data_key_id", "created", "updated" ) VALUES ( @@ -10,6 +11,7 @@ INSERT INTO "secret_encrypted_value" ( 'n1', 1, '[115 101 99 114 101 116]', + 'test-data-key-id', 1234, 5678 ); diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql index 74432ebbf69..ee8b9c0a399 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql index 1d7089f751e..661d7f9ea0a 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_all_until_time.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql index 6f2bbd0b90f..06782d7e40d 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_0.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql index b9f326c8bf0..a64d45afaec 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_list_all-list_limit_10_offset_2.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql index 4bbe1000ce6..437685ae4e7 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql @@ -3,6 +3,7 @@ SELECT "name", "version", "encrypted_data", + "data_key_id", "created", "updated" FROM diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql index b2b5e30ecac..0f710f7456d 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql @@ -2,6 +2,7 @@ UPDATE "secret_encrypted_value" SET "encrypted_data" = '[115 101 99 114 101 116]', + "data_key_id" = 'test-data-key-id', "updated" = 5679 WHERE "namespace" = 'ns' AND diff --git a/pkg/storage/secret/metadata/data/secure_value_lease_inactive.sql b/pkg/storage/secret/metadata/data/secure_value_lease_inactive.sql index 3b95aed3000..cabaaed9aa2 100644 --- a/pkg/storage/secret/metadata/data/secure_value_lease_inactive.sql +++ b/pkg/storage/secret/metadata/data/secure_value_lease_inactive.sql @@ -1,20 +1,19 @@ -WITH to_update AS ( - SELECT guid FROM ( - SELECT - guid, - ROW_NUMBER() OVER (ORDER BY created ASC) AS rn - FROM {{ .Ident "secret_secure_value" }} - WHERE +UPDATE + {{ .Ident "secret_secure_value" }} +SET + {{ .Ident "lease_token" }} = {{ .Arg .LeaseToken }}, + {{ .Ident "lease_created" }} = {{ .Arg .Now }} +WHERE {{ .Ident "guid" }} IN ( + SELECT {{ .Ident "guid" }} FROM ( + SELECT + {{ .Ident "guid" }}, + ROW_NUMBER() OVER (ORDER BY {{ .Ident "created" }} ASC) AS rn + FROM {{ .Ident "secret_secure_value" }} + WHERE {{ .Ident "active" }} = FALSE AND {{ .Arg .Now }} - {{ .Ident "created" }} > {{ .Arg .MinAge }} AND {{ .Arg .Now }} - {{ .Ident "lease_created" }} > {{ .Arg .LeaseTTL }} ) AS sub WHERE rn <= {{ .Arg .MaxBatchSize }} ) -UPDATE - {{ .Ident "secret_secure_value" }} -SET - {{ .Ident "lease_token" }} = {{ .Arg .LeaseToken }}, - {{ .Ident "lease_created" }} = {{ .Arg .Now }} -WHERE guid IN (SELECT guid FROM to_update) -; \ No newline at end of file +; diff --git a/pkg/storage/secret/metadata/decrypt_store.go b/pkg/storage/secret/metadata/decrypt_store.go index 413c4a48b2b..ea9d851999d 100644 --- a/pkg/storage/secret/metadata/decrypt_store.go +++ b/pkg/storage/secret/metadata/decrypt_store.go @@ -145,7 +145,7 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, return "", fmt.Errorf("failed to get keeper for config: %v (%w)", err, contracts.ErrDecryptFailed) } - exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace.String(), name, sv.Status.Version) + exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace, name, sv.Status.Version) if err != nil { return "", fmt.Errorf("failed to expose secret: %v (%w)", err, contracts.ErrDecryptFailed) } diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_lease_inactive-lease inactive.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_lease_inactive-lease inactive.sql index 52d84085dc8..b93f38c4b45 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_lease_inactive-lease inactive.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_lease_inactive-lease inactive.sql @@ -1,20 +1,19 @@ -WITH to_update AS ( - SELECT guid FROM ( - SELECT - guid, - ROW_NUMBER() OVER (ORDER BY created ASC) AS rn - FROM `secret_secure_value` - WHERE +UPDATE + `secret_secure_value` +SET + `lease_token` = 'token', + `lease_created` = 10 +WHERE `guid` IN ( + SELECT `guid` FROM ( + SELECT + `guid`, + ROW_NUMBER() OVER (ORDER BY `created` ASC) AS rn + FROM `secret_secure_value` + WHERE `active` = FALSE AND 10 - `created` > 300 AND 10 - `lease_created` > 30 ) AS sub WHERE rn <= 10 ) -UPDATE - `secret_secure_value` -SET - `lease_token` = 'token', - `lease_created` = 10 -WHERE guid IN (SELECT guid FROM to_update) ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_lease_inactive-lease inactive.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_lease_inactive-lease inactive.sql index 9c0d824458f..e490897ac97 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_lease_inactive-lease inactive.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_lease_inactive-lease inactive.sql @@ -1,20 +1,19 @@ -WITH to_update AS ( - SELECT guid FROM ( - SELECT - guid, - ROW_NUMBER() OVER (ORDER BY created ASC) AS rn - FROM "secret_secure_value" - WHERE +UPDATE + "secret_secure_value" +SET + "lease_token" = 'token', + "lease_created" = 10 +WHERE "guid" IN ( + SELECT "guid" FROM ( + SELECT + "guid", + ROW_NUMBER() OVER (ORDER BY "created" ASC) AS rn + FROM "secret_secure_value" + WHERE "active" = FALSE AND 10 - "created" > 300 AND 10 - "lease_created" > 30 ) AS sub WHERE rn <= 10 ) -UPDATE - "secret_secure_value" -SET - "lease_token" = 'token', - "lease_created" = 10 -WHERE guid IN (SELECT guid FROM to_update) ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_lease_inactive-lease inactive.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_lease_inactive-lease inactive.sql index 9c0d824458f..e490897ac97 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_lease_inactive-lease inactive.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_lease_inactive-lease inactive.sql @@ -1,20 +1,19 @@ -WITH to_update AS ( - SELECT guid FROM ( - SELECT - guid, - ROW_NUMBER() OVER (ORDER BY created ASC) AS rn - FROM "secret_secure_value" - WHERE +UPDATE + "secret_secure_value" +SET + "lease_token" = 'token', + "lease_created" = 10 +WHERE "guid" IN ( + SELECT "guid" FROM ( + SELECT + "guid", + ROW_NUMBER() OVER (ORDER BY "created" ASC) AS rn + FROM "secret_secure_value" + WHERE "active" = FALSE AND 10 - "created" > 300 AND 10 - "lease_created" > 30 ) AS sub WHERE rn <= 10 ) -UPDATE - "secret_secure_value" -SET - "lease_token" = 'token', - "lease_created" = 10 -WHERE guid IN (SELECT guid FROM to_update) ; diff --git a/pkg/storage/secret/migrator/migrator.go b/pkg/storage/secret/migrator/migrator.go index 34b56a905df..b08fb766e96 100644 --- a/pkg/storage/secret/migrator/migrator.go +++ b/pkg/storage/secret/migrator/migrator.go @@ -200,4 +200,15 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { mg.AddMigration("add lease_created index to "+TableNameSecureValue, migrator.NewAddIndexMigration(secureValueTable, &migrator.Index{ Cols: []string{"lease_created"}, })) + + mg.AddMigration("add data_key_id column to "+TableNameEncryptedValue, migrator.NewAddColumnMigration(encryptedValueTable, &migrator.Column{ + Name: "data_key_id", + Type: migrator.DB_NVarchar, + Length: 100, + Nullable: false, + Default: "''", + })) + mg.AddMigration("add data_key_id index to "+TableNameEncryptedValue, migrator.NewAddIndexMigration(encryptedValueTable, &migrator.Index{ + Cols: []string{"data_key_id"}, + })) } diff --git a/pkg/storage/unified/apistore/restoptions.go b/pkg/storage/unified/apistore/restoptions.go index 53ce1d2bb2a..b48fbc6deaf 100644 --- a/pkg/storage/unified/apistore/restoptions.go +++ b/pkg/storage/unified/apistore/restoptions.go @@ -3,13 +3,11 @@ package apistore import ( - "context" "os" "path/filepath" "time" - "gocloud.dev/blob/fileblob" - "gocloud.dev/blob/memblob" + badger "github.com/dgraph-io/badger/v4" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/registry/generic" @@ -53,18 +51,30 @@ func NewRESTOptionsGetterForClient( } func NewRESTOptionsGetterMemory(originalStorageConfig storagebackend.Config, secrets secret.InlineSecureValueSupport) (*RESTOptionsGetter, error) { - backend, err := resource.NewCDKBackend(context.Background(), resource.CDKBackendOptions{ - Bucket: memblob.OpenBucket(&memblob.Options{}), + // Create BadgerDB with in-memory mode + db, err := badger.Open(badger.DefaultOptions(""). + WithInMemory(true). + WithLogger(nil)) + if err != nil { + return nil, err + } + + kv := resource.NewBadgerKV(db) + backend, err := resource.NewKVStorageBackend(resource.KVBackendOptions{ + KvStore: kv, + WithExperimentalClusterScope: true, }) if err != nil { return nil, err } + server, err := resource.NewResourceServer(resource.ResourceServerOptions{ Backend: backend, }) if err != nil { return nil, err } + return NewRESTOptionsGetterForClient( resource.NewLocalResourceClient(server), secrets, @@ -83,25 +93,27 @@ func NewRESTOptionsGetterForFileXX(path string, path = filepath.Join(os.TempDir(), "grafana-apiserver") } - bucket, err := fileblob.OpenBucket(filepath.Join(path, "resource"), &fileblob.Options{ - CreateDir: true, - Metadata: fileblob.MetadataDontWrite, // skip - }) + db, err := badger.Open(badger.DefaultOptions(filepath.Join(path, "badger")). + WithLogger(nil)) if err != nil { return nil, err } - backend, err := resource.NewCDKBackend(context.Background(), resource.CDKBackendOptions{ - Bucket: bucket, + + kv := resource.NewBadgerKV(db) + backend, err := resource.NewKVStorageBackend(resource.KVBackendOptions{ + KvStore: kv, }) if err != nil { return nil, err } + server, err := resource.NewResourceServer(resource.ResourceServerOptions{ Backend: backend, }) if err != nil { return nil, err } + return NewRESTOptionsGetterForClient( resource.NewLocalResourceClient(server), nil, // secrets diff --git a/pkg/storage/unified/apistore/store.go b/pkg/storage/unified/apistore/store.go index a6c37c00dab..2ad474dc90f 100644 --- a/pkg/storage/unified/apistore/store.go +++ b/pkg/storage/unified/apistore/store.go @@ -18,6 +18,7 @@ import ( "time" "github.com/bwmarrin/snowflake" + "go.opentelemetry.io/otel" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -46,7 +47,10 @@ const ( LargeObjectSupportDisabled = false ) -var _ storage.Interface = (*Storage)(nil) +var ( + _ storage.Interface = (*Storage)(nil) + tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/apistore") +) type DefaultPermissionSetter = func(ctx context.Context, key *resourcepb.ResourceKey, id authtypes.AuthInfo, obj utils.GrafanaMetaAccessor) error @@ -200,7 +204,9 @@ func (s *Storage) Versioner() storage.Versioner { return s.versioner } -func (s *Storage) convertToObject(data []byte, obj runtime.Object) (runtime.Object, error) { +func (s *Storage) convertToObject(ctx context.Context, data []byte, obj runtime.Object) (runtime.Object, error) { + _, span := tracer.Start(ctx, "apistore.Storage.convertToObject") + defer span.End() obj, _, err := s.codec.Decode(data, nil, obj) return obj, err } @@ -209,6 +215,8 @@ func (s *Storage) convertToObject(data []byte, obj runtime.Object) (runtime.Obje // in seconds (0 means forever). If no error is returned and out is not nil, out will be // set to the read value from database. func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, out runtime.Object, ttl uint64) error { + ctx, span := tracer.Start(ctx, "apistore.Storage.Create") + defer span.End() v, err := s.prepareObjectForStorage(ctx, obj) if err != nil { return s.handleManagedResourceRouting(ctx, err, resourcepb.WatchEvent_ADDED, key, obj, out) @@ -238,7 +246,7 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou return v.finish(ctx, err, s.opts.SecureValues) } - if _, err := s.convertToObject(req.Value, out); err != nil { + if _, err := s.convertToObject(ctx, req.Value, out); err != nil { return err } @@ -274,6 +282,8 @@ func (s *Storage) Delete( _ runtime.Object, opts storage.DeleteOptions, ) error { + ctx, span := tracer.Start(ctx, "apistore.Storage.Delete") + defer span.End() info, ok := authtypes.AuthInfoFrom(ctx) if !ok { return errors.New("missing auth info") @@ -336,6 +346,8 @@ func (s *Storage) Delete( // This version is not yet passing the watch tests func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { + ctx, span := tracer.Start(ctx, "apistore.Storage.Watch") + defer span.End() k, err := s.getKey(key) if err != nil { return watch.NewEmptyWatch(), nil @@ -379,6 +391,8 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption // The returned contents may be delayed, but it is guaranteed that they will // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error { + ctx, span := tracer.Start(ctx, "apistore.Storage.Get") + defer span.End() var err error req := &resourcepb.ReadRequest{} req.Key, err = s.getKey(key) @@ -410,7 +424,7 @@ func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, return resource.GetError(rsp.Error) } - _, err = s.convertToObject(rsp.Value, objPtr) + _, err = s.convertToObject(ctx, rsp.Value, objPtr) if err != nil { return err } @@ -424,6 +438,8 @@ func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, // The returned contents may be delayed, but it is guaranteed that they will // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + ctx, span := tracer.Start(ctx, "apistore.Storage.GetList") + defer span.End() k, err := s.getKey(key) if err != nil { return err @@ -460,30 +476,11 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti } for _, item := range rsp.Items { - obj, err := s.convertToObject(item.Value, s.newFunc()) + obj, shouldAppend, err := s.processItem(ctx, item, opts, predicate) if err != nil { return err } - if err := s.versioner.UpdateObject(obj, uint64(item.ResourceVersion)); err != nil { - return err - } - - if opts.ResourceVersionMatch == metaV1.ResourceVersionMatchExact { - currentVersion, err := s.versioner.ObjectResourceVersion(obj) - if err != nil { - return err - } - expectedRV, err := s.versioner.ParseResourceVersion(opts.ResourceVersion) - if err != nil { - return err - } - if currentVersion != expectedRV { - continue - } - } - - ok, err := predicate.Matches(obj) - if err == nil && ok { + if shouldAppend { v.Set(reflect.Append(v, reflect.ValueOf(obj).Elem())) } } @@ -498,6 +495,45 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti return nil } +// processItem converts a raw item from the response, updates its version, +// checks resource version matching, and applies predicates. +// Returns the processed object, whether it should be appended to results, and any error. +func (s *Storage) processItem(ctx context.Context, item *resourcepb.ResourceWrapper, opts storage.ListOptions, predicate storage.SelectionPredicate) (runtime.Object, bool, error) { + _, span := tracer.Start(ctx, "apistore.Storage.processItem") + defer span.End() + + obj, err := s.convertToObject(ctx, item.Value, s.newFunc()) + if err != nil { + return nil, false, err + } + if err := s.versioner.UpdateObject(obj, uint64(item.ResourceVersion)); err != nil { + return nil, false, err + } + + // Skip items that don't match the exact resource version if specified + if opts.ResourceVersionMatch == metaV1.ResourceVersionMatchExact { + currentVersion, err := s.versioner.ObjectResourceVersion(obj) + if err != nil { + return nil, false, err + } + expectedRV, err := s.versioner.ParseResourceVersion(opts.ResourceVersion) + if err != nil { + return nil, false, err + } + if currentVersion != expectedRV { + return nil, false, nil + } + } + + // Apply predicate filtering + ok, err := predicate.Matches(obj) + if err != nil || !ok { + return nil, false, nil + } + + return obj, true, nil +} + // GuaranteedUpdate keeps calling 'tryUpdate()' to update key 'key' (of type 'destination') // retrying the update until success if there is index conflict. // Note that object passed to tryUpdate may change across invocations of tryUpdate() if @@ -521,6 +557,8 @@ func (s *Storage) GuaranteedUpdate( tryUpdate storage.UpdateFunc, cachedExistingObject runtime.Object, ) error { + ctx, span := tracer.Start(ctx, "apistore.Storage.GuaranteedUpdate") + defer span.End() var ( res storage.ResponseMeta updatedObj runtime.Object @@ -586,7 +624,7 @@ func (s *Storage) GuaranteedUpdate( } existingBytes = readResponse.Value - existingObj, err = s.convertToObject(readResponse.Value, s.newFunc()) + existingObj, err = s.convertToObject(ctx, readResponse.Value, s.newFunc()) if err != nil { return err } @@ -649,7 +687,7 @@ func (s *Storage) GuaranteedUpdate( rv = uint64(updateResponse.ResourceVersion) } - if _, err := s.convertToObject(req.Value, destination); err != nil { + if _, err := s.convertToObject(ctx, req.Value, destination); err != nil { return err } diff --git a/pkg/storage/unified/apistore/watcher_test.go b/pkg/storage/unified/apistore/watcher_test.go index afd2a77b08b..d23c8700c90 100644 --- a/pkg/storage/unified/apistore/watcher_test.go +++ b/pkg/storage/unified/apistore/watcher_test.go @@ -8,15 +8,13 @@ package apistore_test import ( "context" "fmt" - "os" "strings" "testing" "time" + badger "github.com/dgraph-io/badger/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gocloud.dev/blob/fileblob" - "gocloud.dev/blob/memblob" "k8s.io/apimachinery/pkg/api/apitesting" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -105,24 +103,20 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte Resource: "pods", } - bucket := memblob.OpenBucket(nil) - if true { - tmp, err := os.MkdirTemp("", "xxx-*") - require.NoError(t, err) - - bucket, err = fileblob.OpenBucket(tmp, &fileblob.Options{ - CreateDir: true, - Metadata: fileblob.MetadataDontWrite, // skip - }) - require.NoError(t, err) - } ctx := storagetesting.NewContext() var server resource.ResourceServer switch setupOpts.storageType { case StorageTypeFile: - backend, err := resource.NewCDKBackend(ctx, resource.CDKBackendOptions{ - Bucket: bucket, + // Create in-memory BadgerDB for testing + db, err := badger.Open(badger.DefaultOptions(""). + WithInMemory(true). + WithLogger(nil)) + require.NoError(t, err) + + kv := resource.NewBadgerKV(db) + backend, err := resource.NewKVStorageBackend(resource.KVBackendOptions{ + KvStore: kv, }) require.NoError(t, err) diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index b0e7f6cf275..0a4d3e630ff 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -6,12 +6,12 @@ import ( "path/filepath" "time" + badger "github.com/dgraph-io/badger/v4" otgrpc "github.com/opentracing-contrib/go-grpc" "github.com/opentracing/opentracing-go" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" - "gocloud.dev/blob/fileblob" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/keepalive" @@ -111,19 +111,22 @@ func newClient(opts options.StorageOptions, if opts.DataPath == "" { opts.DataPath = filepath.Join(cfg.DataPath, "grafana-apiserver") } - bucket, err := fileblob.OpenBucket(filepath.Join(opts.DataPath, "resource"), &fileblob.Options{ - CreateDir: true, - Metadata: fileblob.MetadataDontWrite, // skip - }) + + // Create BadgerDB instance + db, err := badger.Open(badger.DefaultOptions(filepath.Join(opts.DataPath, "badger")). + WithLogger(nil)) if err != nil { return nil, err } - backend, err := resource.NewCDKBackend(ctx, resource.CDKBackendOptions{ - Bucket: bucket, + + kv := resource.NewBadgerKV(db) + backend, err := resource.NewKVStorageBackend(resource.KVBackendOptions{ + KvStore: kv, }) if err != nil { return nil, err } + server, err := resource.NewResourceServer(resource.ResourceServerOptions{ Backend: backend, Blob: resource.BlobConfig{ diff --git a/pkg/storage/unified/parquet/writer.go b/pkg/storage/unified/parquet/writer.go index 1b418526de8..c35cf69b38b 100644 --- a/pkg/storage/unified/parquet/writer.go +++ b/pkg/storage/unified/parquet/writer.go @@ -114,9 +114,7 @@ func (w *parquetWriter) Close() error { // writes the current buffer to parquet and re-inits the arrow buffer func (w *parquetWriter) flush() error { w.logger.Info("flush", "count", w.rv.Len()) - //TODO: fix deprecation warning - //nolint:staticcheck - rec := array.NewRecord(w.schema, []arrow.Array{ + rec := array.NewRecordBatch(w.schema, []arrow.Array{ w.rv.NewArray(), w.namespace.NewArray(), w.group.NewArray(), diff --git a/pkg/storage/unified/resource/bulk.go b/pkg/storage/unified/resource/bulk.go index 935204384df..1ac0ff2032f 100644 --- a/pkg/storage/unified/resource/bulk.go +++ b/pkg/storage/unified/resource/bulk.go @@ -110,7 +110,7 @@ func NewBulkSettings(md metadata.MD) (BulkSettings, error) { // All requests must be to the same NAMESPACE/GROUP/RESOURCE func (s *server) BulkProcess(stream resourcepb.BulkStore_BulkProcessServer) error { ctx := stream.Context() - ctx, span := s.tracer.Start(ctx, "resource.server.BulkProcess") + ctx, span := tracer.Start(ctx, "resource.server.BulkProcess") defer span.End() sendAndClose := func(rsp *resourcepb.BulkResponse) error { diff --git a/pkg/storage/unified/resource/cdk_backend.go b/pkg/storage/unified/resource/cdk_backend.go deleted file mode 100644 index 0cb9628758a..00000000000 --- a/pkg/storage/unified/resource/cdk_backend.go +++ /dev/null @@ -1,418 +0,0 @@ -package resource - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "iter" - "net/http" - "sort" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" - "gocloud.dev/blob" - _ "gocloud.dev/blob/fileblob" - _ "gocloud.dev/blob/memblob" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" -) - -type CDKBackendOptions struct { - Tracer trace.Tracer - Bucket CDKBucket - RootFolder string -} - -func NewCDKBackend(ctx context.Context, opts CDKBackendOptions) (StorageBackend, error) { - if opts.Tracer == nil { - opts.Tracer = noop.NewTracerProvider().Tracer("cdk-appending-store") - } - - if opts.Bucket == nil { - return nil, fmt.Errorf("missing bucket") - } - - found, _, err := opts.Bucket.ListPage(ctx, blob.FirstPageToken, 1, &blob.ListOptions{ - Prefix: opts.RootFolder, - Delimiter: "/", - }) - if err != nil { - return nil, err - } - if found == nil { - return nil, fmt.Errorf("the root folder does not exist") - } - - backend := &cdkBackend{ - tracer: opts.Tracer, - bucket: opts.Bucket, - root: opts.RootFolder, - } - backend.rv.Swap(time.Now().UnixMilli()) - return backend, nil -} - -type cdkBackend struct { - tracer trace.Tracer - bucket CDKBucket - root string - - mutex sync.Mutex - rv atomic.Int64 - - // Simple watch stream -- NOTE, this only works for single tenant! - broadcaster Broadcaster[*WrittenEvent] - stream chan<- *WrittenEvent -} - -func (s *cdkBackend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[ResourceLastImportTime, error] { - return func(yield func(ResourceLastImportTime, error) bool) { - yield(ResourceLastImportTime{}, errors.New("not implemented")) - } -} - -func (s *cdkBackend) ListModifiedSince(ctx context.Context, key NamespacedResource, sinceRv int64) (int64, iter.Seq2[*ModifiedResource, error]) { - return 0, func(yield func(*ModifiedResource, error) bool) { - yield(nil, errors.New("not implemented")) - } -} - -func (s *cdkBackend) getPath(key *resourcepb.ResourceKey, rv int64) string { - var buffer bytes.Buffer - buffer.WriteString(s.root) - - if key.Group == "" { - return buffer.String() - } - buffer.WriteString(key.Group) - - if key.Resource == "" { - return buffer.String() - } - buffer.WriteString("/") - buffer.WriteString(key.Resource) - - if key.Namespace == "" { - if key.Name == "" { - return buffer.String() - } - buffer.WriteString("/__cluster__") - } else { - buffer.WriteString("/") - buffer.WriteString(key.Namespace) - } - - if key.Name == "" { - return buffer.String() - } - buffer.WriteString("/") - buffer.WriteString(key.Name) - - if rv > 0 { - buffer.WriteString(fmt.Sprintf("/%d.json", rv)) - } - return buffer.String() -} - -// GetResourceStats implements Backend. -func (s *cdkBackend) GetResourceStats(ctx context.Context, namespace string, minCount int) ([]ResourceStats, error) { - return nil, fmt.Errorf("not implemented") -} - -func (s *cdkBackend) WriteEvent(ctx context.Context, event WriteEvent) (rv int64, err error) { - if event.Type == resourcepb.WatchEvent_ADDED { - // ReadResource deals with deleted values (i.e. a file exists but has generation -999). - resp := s.ReadResource(ctx, &resourcepb.ReadRequest{Key: event.Key}) - if resp.Error != nil && resp.Error.Code != http.StatusNotFound { - return 0, GetError(resp.Error) - } - if resp.Value != nil { - return 0, ErrResourceAlreadyExists - } - } - - // Scope the lock - { - s.mutex.Lock() - defer s.mutex.Unlock() - - rv = s.rv.Add(1) - err = s.bucket.WriteAll(ctx, s.getPath(event.Key, rv), event.Value, &blob.WriterOptions{ - ContentType: "application/json", - }) - } - - // notify all subscribers - if s.stream != nil { - write := &WrittenEvent{ - Type: event.Type, - Key: event.Key, - PreviousRV: event.PreviousRV, - Value: event.Value, - Timestamp: time.Now().UnixMilli(), - ResourceVersion: rv, - } - s.stream <- write - } - return rv, err -} - -func (s *cdkBackend) ReadResource(ctx context.Context, req *resourcepb.ReadRequest) *BackendReadResponse { - rv := req.ResourceVersion - - path := s.getPath(req.Key, rv) - if rv < 1 { - iter := s.bucket.List(&blob.ListOptions{Prefix: path + "/", Delimiter: "/"}) - for { - obj, err := iter.Next(ctx) - if errors.Is(err, io.EOF) { - break - } - if strings.HasSuffix(obj.Key, ".json") { - idx := strings.LastIndex(obj.Key, "/") + 1 - edx := strings.LastIndex(obj.Key, ".") - if idx > 0 { - v, err := strconv.ParseInt(obj.Key[idx:edx], 10, 64) - if err == nil && v > rv { - rv = v - path = obj.Key // find the path with biggest resource version - } - } - } - } - } - - raw, err := s.bucket.ReadAll(ctx, path) - if raw == nil && req.ResourceVersion > 0 { - if req.ResourceVersion > s.rv.Load() { - return &BackendReadResponse{ - Error: &resourcepb.ErrorResult{ - Code: http.StatusGatewayTimeout, - Reason: string(metav1.StatusReasonTimeout), // match etcd behavior - Message: "ResourceVersion is larger than max", - Details: &resourcepb.ErrorDetails{ - Causes: []*resourcepb.ErrorCause{ - { - Reason: string(metav1.CauseTypeResourceVersionTooLarge), - Message: fmt.Sprintf("requested: %d, current %d", req.ResourceVersion, s.rv.Load()), - }, - }, - }, - }, - } - } - - // If the there was an explicit request, get the latest - rsp := s.ReadResource(ctx, &resourcepb.ReadRequest{Key: req.Key}) - if rsp != nil && len(rsp.Value) > 0 { - raw = rsp.Value - rv = rsp.ResourceVersion - err = nil - } - } - if err == nil && isDeletedValue(raw) { - raw = nil - } - if raw == nil { - return &BackendReadResponse{Error: NewNotFoundError(req.Key)} - } - return &BackendReadResponse{ - Key: req.Key, - Folder: "", // TODO: implement this - ResourceVersion: rv, - Value: raw, - } -} - -func isDeletedValue(raw []byte) bool { - if bytes.Contains(raw, []byte(`"generation":-999`)) { - tmp := &unstructured.Unstructured{} - err := tmp.UnmarshalJSON(raw) - if err == nil && tmp.GetGeneration() == utils.DeletedGeneration { - return true - } - } - return false -} - -func (s *cdkBackend) ListIterator(ctx context.Context, req *resourcepb.ListRequest, cb func(ListIterator) error) (int64, error) { - resources, err := buildTree(ctx, s, req.Options.Key) - if err != nil { - return 0, err - } - err = cb(resources) - return resources.listRV, err -} - -func (s *cdkBackend) ListHistory(ctx context.Context, req *resourcepb.ListRequest, cb func(ListIterator) error) (int64, error) { - return 0, fmt.Errorf("listing from history not supported in CDK backend") -} - -func (s *cdkBackend) WatchWriteEvents(ctx context.Context) (<-chan *WrittenEvent, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - if s.broadcaster == nil { - var err error - s.broadcaster, err = NewBroadcaster(context.Background(), func(c chan<- *WrittenEvent) error { - s.stream = c - return nil - }) - if err != nil { - return nil, err - } - } - return s.broadcaster.Subscribe(ctx) -} - -// group > resource > namespace > name > versions -type cdkResource struct { - prefix string - versions []cdkVersion -} -type cdkVersion struct { - rv int64 - key string -} - -type cdkListIterator struct { - bucket CDKBucket - ctx context.Context - err error - - listRV int64 - resources []cdkResource - index int - - currentRV int64 - currentKey string - currentVal []byte -} - -// Next implements ListIterator. -func (c *cdkListIterator) Next() bool { - if c.err != nil { - return false - } - for { - c.currentVal = nil - c.index += 1 - if c.index >= len(c.resources) { - return false - } - - item := c.resources[c.index] - latest := item.versions[0] - raw, err := c.bucket.ReadAll(c.ctx, latest.key) - if err != nil { - c.err = err - return false - } - if !isDeletedValue(raw) { - c.currentRV = latest.rv - c.currentKey = latest.key - c.currentVal = raw - return true - } - } -} - -// Error implements ListIterator. -func (c *cdkListIterator) Error() error { - return c.err -} - -// ResourceVersion implements ListIterator. -func (c *cdkListIterator) ResourceVersion() int64 { - return c.currentRV -} - -// Value implements ListIterator. -func (c *cdkListIterator) Value() []byte { - return c.currentVal -} - -// ContinueToken implements ListIterator. -func (c *cdkListIterator) ContinueToken() string { - return fmt.Sprintf("index:%d/key:%s", c.index, c.currentKey) -} - -// Name implements ListIterator. -func (c *cdkListIterator) Name() string { - return c.currentKey // TODO (parse name from key) -} - -// Namespace implements ListIterator. -func (c *cdkListIterator) Namespace() string { - return c.currentKey // TODO (parse namespace from key) -} - -func (c *cdkListIterator) Folder() string { - return "" // TODO: implement this -} - -var _ ListIterator = (*cdkListIterator)(nil) - -func buildTree(ctx context.Context, s *cdkBackend, key *resourcepb.ResourceKey) (*cdkListIterator, error) { - byPrefix := make(map[string]*cdkResource) - path := s.getPath(key, 0) - iter := s.bucket.List(&blob.ListOptions{Prefix: path, Delimiter: ""}) // "" is recursive - for { - obj, err := iter.Next(ctx) - if errors.Is(err, io.EOF) { - break - } - if strings.HasSuffix(obj.Key, ".json") { - idx := strings.LastIndex(obj.Key, "/") + 1 - edx := strings.LastIndex(obj.Key, ".") - if idx > 0 { - rv, err := strconv.ParseInt(obj.Key[idx:edx], 10, 64) - if err == nil { - prefix := obj.Key[:idx] - res, ok := byPrefix[prefix] - if !ok { - res = &cdkResource{prefix: prefix} - byPrefix[prefix] = res - } - - res.versions = append(res.versions, cdkVersion{ - rv: rv, - key: obj.Key, - }) - } - } - } - } - - // Now sort all versions - resources := make([]cdkResource, 0, len(byPrefix)) - for _, res := range byPrefix { - sort.Slice(res.versions, func(i, j int) bool { - return res.versions[i].rv > res.versions[j].rv - }) - resources = append(resources, *res) - } - sort.Slice(resources, func(i, j int) bool { - a := resources[i].prefix - b := resources[j].prefix - return a < b - }) - - return &cdkListIterator{ - ctx: ctx, - bucket: s.bucket, - resources: resources, - listRV: s.rv.Load(), - index: -1, // must call next first - }, nil -} diff --git a/pkg/storage/unified/resource/datastore.go b/pkg/storage/unified/resource/datastore.go index 8ecb878b675..eabacd2da1a 100644 --- a/pkg/storage/unified/resource/datastore.go +++ b/pkg/storage/unified/resource/datastore.go @@ -77,8 +77,10 @@ func (k DataKey) Validate() error { } // Validate naming conventions for all required fields - if err := validation.IsValidNamespace(k.Namespace); err != nil { - return NewValidationError("namespace", k.Namespace, err[0]) + if k.Namespace != clusterScopeNamespace { + if err := validation.IsValidNamespace(k.Namespace); err != nil { + return NewValidationError("namespace", k.Namespace, err[0]) + } } if err := validation.IsValidGroup(k.Group); err != nil { return NewValidationError("group", k.Group, err[0]) @@ -117,7 +119,7 @@ func (k ListRequestKey) Validate() error { if k.Namespace == "" && k.Name != "" { return errors.New(ErrNameMustBeEmptyWhenNamespaceEmpty) } - if k.Namespace != "" { + if k.Namespace != "" && k.Namespace != clusterScopeNamespace { if err := validation.IsValidNamespace(k.Namespace); err != nil { return NewValidationError("namespace", k.Namespace, err[0]) } @@ -155,8 +157,10 @@ func (k GetRequestKey) Validate() error { if k.Namespace == "" { return errors.New(ErrNamespaceRequired) } - if err := validation.IsValidNamespace(k.Namespace); err != nil { - return NewValidationError("namespace", k.Namespace, err[0]) + if k.Namespace != clusterScopeNamespace { + if err := validation.IsValidNamespace(k.Namespace); err != nil { + return NewValidationError("namespace", k.Namespace, err[0]) + } } if err := validation.IsValidGroup(k.Group); err != nil { return NewValidationError("group", k.Group, err[0]) @@ -364,7 +368,7 @@ func (d *dataStore) Get(ctx context.Context, key DataKey) (io.ReadCloser, error) // BatchGet retrieves multiple data objects in batches. // It returns an iterator that yields DataObj results for the given keys. -// Keys are processed in batches (default 50) to balance between efficiency and memory usage. +// Keys are processed in batches (default 50). // Non-existent entries will not appear in the result. func (d *dataStore) BatchGet(ctx context.Context, keys []DataKey) iter.Seq2[DataObj, error] { return func(yield func(DataObj, error) bool) { diff --git a/pkg/storage/unified/resource/eventstore.go b/pkg/storage/unified/resource/eventstore.go index 04a35606f81..7f80fb6b87a 100644 --- a/pkg/storage/unified/resource/eventstore.go +++ b/pkg/storage/unified/resource/eventstore.go @@ -15,7 +15,8 @@ import ( ) const ( - eventsSection = "unified/events" + eventsSection = "unified/events" + deleteEventBatchSize = 50 ) // eventStore is a store for events. @@ -50,8 +51,10 @@ func (k EventKey) Validate() error { // Validate each field against the naming rules // Validate naming conventions for all required fields - if err := validation.IsValidNamespace(k.Namespace); err != nil { - return NewValidationError("namespace", k.Namespace, err[0]) + if k.Namespace != clusterScopeNamespace { + if err := validation.IsValidNamespace(k.Namespace); err != nil { + return NewValidationError("namespace", k.Namespace, err[0]) + } } if err := validation.IsValidGroup(k.Group); err != nil { return NewValidationError("group", k.Group, err[0]) @@ -182,10 +185,8 @@ func (n *eventStore) Get(ctx context.Context, key EventKey) (Event, error) { // ListSince returns a sequence of events since the given resource version. func (n *eventStore) ListKeysSince(ctx context.Context, sinceRV int64) iter.Seq2[string, error] { opts := ListOptions{ - Sort: SortOrderAsc, - StartKey: EventKey{ - ResourceVersion: sinceRV, - }.String(), + Sort: SortOrderAsc, + StartKey: fmt.Sprintf("%d", sinceRV), } return func(yield func(string, error) bool) { for evtKey, err := range n.kv.Keys(ctx, eventsSection, opts) { @@ -231,27 +232,59 @@ func (n *eventStore) ListSince(ctx context.Context, sinceRV int64) iter.Seq2[Eve // CleanupOldEvents deletes events older than the specified retention period. func (n *eventStore) CleanupOldEvents(ctx context.Context, cutoff time.Time) (int, error) { - deletedCount := 0 - // Keys are stored in the format of "resource_version~namespace~group~resource~name" // With a start key of "1" and an end key of the cutoff time we can get all expired events. endKey := fmt.Sprintf("%d", snowflakeFromTime(cutoff)) + + // Collect keys to delete + keysToDelete := make([]string, 0, deleteEventBatchSize) for key, err := range n.kv.Keys(ctx, eventsSection, ListOptions{StartKey: "1", EndKey: endKey}) { if err != nil { - return deletedCount, fmt.Errorf("failed to list event keys: %w", err) + return 0, fmt.Errorf("failed to list event keys: %w", err) } - - // TODO should use batch deletes here when available - if err := n.kv.Delete(ctx, eventsSection, key); err != nil { - return deletedCount, fmt.Errorf("failed to delete event key %s: %w", key, err) - } - deletedCount++ + keysToDelete = append(keysToDelete, key) } - return deletedCount, nil + // Use batch delete + if err := n.batchDelete(ctx, keysToDelete); err != nil { + return 0, fmt.Errorf("failed to batch delete events: %w", err) + } + + return len(keysToDelete), nil +} + +// batchDelete deletes multiple events in batches. +// Keys are processed in batches (default 50). +func (n *eventStore) batchDelete(ctx context.Context, keys []string) error { + for len(keys) > 0 { + batch := keys + if len(batch) > deleteEventBatchSize { + batch = batch[:deleteEventBatchSize] + } + keys = keys[len(batch):] + + if err := n.kv.BatchDelete(ctx, eventsSection, batch); err != nil { + return err + } + } + + return nil } // snowflake id with last two sections set to 0 (machine id and sequence) func snowflakeFromTime(t time.Time) int64 { return (t.UnixMilli() - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits) } + +// subtractDurationFromSnowflake subtracts a duration from a snowflake ID by +// converting it to time, subtracting the duration, and converting back to a snowflake ID +func subtractDurationFromSnowflake(snowflakeID int64, duration time.Duration) int64 { + // Extract timestamp from snowflake (returns milliseconds since epoch) + timestamp := snowflake.ID(snowflakeID).Time() + // Convert to time.Time + t := time.Unix(0, timestamp*int64(time.Millisecond)) + // Subtract duration + newTime := t.Add(-duration) + // Convert back to snowflake + return snowflakeFromTime(newTime) +} diff --git a/pkg/storage/unified/resource/eventstore_test.go b/pkg/storage/unified/resource/eventstore_test.go index b846536e864..a9d2ee93eb4 100644 --- a/pkg/storage/unified/resource/eventstore_test.go +++ b/pkg/storage/unified/resource/eventstore_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/bwmarrin/snowflake" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -610,3 +611,183 @@ func TestEventStore_CleanupOldEvents_EmptyStore(t *testing.T) { require.NoError(t, err) assert.Equal(t, 0, deletedCount, "Should not have deleted any events from empty store") } + +func TestEventStore_BatchDelete(t *testing.T) { + ctx := context.Background() + store := setupTestEventStore(t) + + // Create multiple events (more than batch size to test batching) + eventKeys := make([]string, 75) + for i := 0; i < 75; i++ { + event := Event{ + Namespace: "default", + Group: "apps", + Resource: "deployments", + Name: "test-deployment", + ResourceVersion: int64(1000 + i), + Action: DataActionCreated, + Folder: "test-folder", + PreviousRV: int64(999 + i), + } + err := store.Save(ctx, event) + require.NoError(t, err) + + eventKeys[i] = EventKey{ + Namespace: event.Namespace, + Group: event.Group, + Resource: event.Resource, + Name: event.Name, + ResourceVersion: event.ResourceVersion, + Action: event.Action, + Folder: event.Folder, + }.String() + } + + // Batch delete all events + err := store.batchDelete(ctx, eventKeys) + require.NoError(t, err) + + // Verify all events were deleted + for i := 0; i < 75; i++ { + _, err := store.Get(ctx, EventKey{ + Namespace: "default", + Group: "apps", + Resource: "deployments", + Name: "test-deployment", + ResourceVersion: int64(1000 + i), + Action: DataActionCreated, + Folder: "test-folder", + }) + require.Error(t, err, "Event should have been deleted") + } +} + +func TestSubtractDurationFromSnowflake(t *testing.T) { + baseTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + addTime time.Duration + }{ + { + name: "subtract 1 hour", + addTime: -1 * time.Hour, + }, + { + name: "subtract 2 hours", + addTime: -2 * time.Hour, + }, + { + name: "subtract 24 hours", + addTime: -24 * time.Hour, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Generate a snowflake from the base time + baseSnowflake := snowflakeFromTime(baseTime) + + // Subtract the duration + resultSnowflake := subtractDurationFromSnowflake(baseSnowflake, tt.addTime) + + // Convert back to timestamp and verify + // Extract timestamp from the result snowflake + timestamp := snowflake.ID(resultSnowflake).Time() + resultTime := time.Unix(0, timestamp*int64(time.Millisecond)) + + // Compare with expected time (allowing for small differences due to snowflake precision) + expectedMillis := baseTime.Add(-tt.addTime).UnixMilli() + resultMillis := resultTime.UnixMilli() + assert.InDelta(t, expectedMillis, resultMillis, 1, + "Expected time %v, got %v (diff: %d ms)", + baseTime, resultTime, expectedMillis-resultMillis) + }) + } +} + +func TestSnowflakeFromTime(t *testing.T) { + testTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) + snowflakeID := snowflakeFromTime(testTime) + + // Extract timestamp and verify it matches + timestamp := snowflake.ID(snowflakeID).Time() + reconstructedTime := time.Unix(0, timestamp*int64(time.Millisecond)) + + // The times should match at millisecond precision + expectedMillis := testTime.UnixMilli() + resultMillis := reconstructedTime.UnixMilli() + + assert.Equal(t, expectedMillis, resultMillis, "Snowflake timestamp should match original time at millisecond precision") +} + +func TestListKeysSince_WithSnowflakeTime(t *testing.T) { + ctx := context.Background() + store := setupTestEventStore(t) + + // Create events with snowflake-based resource versions at different times + now := time.Now() + events := []Event{ + { + Namespace: "default", + Group: "apps", + Resource: "resource", + Name: "test-1", + ResourceVersion: snowflakeFromTime(now.Add(-2 * time.Hour)), + Action: DataActionCreated, + }, + { + Namespace: "default", + Group: "apps", + Resource: "resource", + Name: "test-2", + ResourceVersion: snowflakeFromTime(now.Add(-1 * time.Hour)), + Action: DataActionUpdated, + }, + { + Namespace: "default", + Group: "apps", + Resource: "resource", + Name: "test-3", + ResourceVersion: snowflakeFromTime(now.Add(-30 * time.Minute)), + Action: DataActionDeleted, + }, + } + + // Save all events + for _, event := range events { + err := store.Save(ctx, event) + require.NoError(t, err) + } + + // List events since 90 minutes ago using subtractDurationFromSnowflake + sinceRV := subtractDurationFromSnowflake(snowflakeFromTime(now), 90*time.Minute) + retrievedEvents := make([]string, 0) + for eventKey, err := range store.ListKeysSince(ctx, sinceRV) { + require.NoError(t, err) + retrievedEvents = append(retrievedEvents, eventKey) + } + + // Should return events from the last hour and 30 minutes + require.Len(t, retrievedEvents, 2) + evt1, err := ParseEventKey(retrievedEvents[0]) + require.NoError(t, err) + assert.Equal(t, "test-2", evt1.Name) + evt2, err := ParseEventKey(retrievedEvents[1]) + require.NoError(t, err) + assert.Equal(t, "test-3", evt2.Name) + + // List events since 30 minutes ago using subtractDurationFromSnowflake + sinceRV = subtractDurationFromSnowflake(snowflakeFromTime(now), 30*time.Minute) + retrievedEvents = make([]string, 0) + for eventKey, err := range store.ListKeysSince(ctx, sinceRV) { + require.NoError(t, err) + retrievedEvents = append(retrievedEvents, eventKey) + } + + // Should return events from the last hour and 30 minutes + require.Len(t, retrievedEvents, 1) + evt, err := ParseEventKey(retrievedEvents[0]) + require.NoError(t, err) + assert.Equal(t, "test-3", evt.Name) +} diff --git a/pkg/storage/unified/resource/keys.go b/pkg/storage/unified/resource/keys.go index 8c9b4b42a93..8ee58c38793 100644 --- a/pkg/storage/unified/resource/keys.go +++ b/pkg/storage/unified/resource/keys.go @@ -35,8 +35,10 @@ func verifyRequestKeyNamespaceGroupResource(key *resourcepb.ResourceKey) *resour if key.Resource == "" { return NewBadRequestError("request key is missing resource") } - if err := validation.IsValidNamespace(key.Namespace); err != nil { - return NewBadRequestError(err[0]) + if key.Namespace != clusterScopeNamespace { + if err := validation.IsValidNamespace(key.Namespace); err != nil { + return NewBadRequestError(err[0]) + } } if err := validation.IsValidGroup(key.Group); err != nil { return NewBadRequestError(err[0]) diff --git a/pkg/storage/unified/resource/notifier.go b/pkg/storage/unified/resource/notifier.go index d0b27e4ad11..f55db60623a 100644 --- a/pkg/storage/unified/resource/notifier.go +++ b/pkg/storage/unified/resource/notifier.go @@ -73,7 +73,7 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event { initialRV, err := n.lastEventResourceVersion(ctx) if errors.Is(err, ErrNotFound) { - initialRV = 0 // No events yet, start from the beginning + initialRV = snowflakeFromTime(time.Now()) // No events yet, start from the beginning } else if err != nil { n.log.Error("Failed to get last event resource version", "error", err) } @@ -86,7 +86,7 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event { case <-ctx.Done(): return case <-time.After(opts.PollInterval): - for evt, err := range n.eventStore.ListSince(ctx, lastRV-opts.LookbackPeriod.Nanoseconds()) { + for evt, err := range n.eventStore.ListSince(ctx, subtractDurationFromSnowflake(lastRV, opts.LookbackPeriod)) { if err != nil { n.log.Error("Failed to list events since", "error", err) continue diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index edaaa0abba6..0f3e9f95912 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -127,11 +127,8 @@ type SearchBackend interface { GetOpenIndexes() []NamespacedResource } -const tracingPrexfixSearch = "unified_search." - // This supports indexing+search regardless of implementation type searchSupport struct { - tracer trace.Tracer log *slog.Logger storage StorageBackend search SearchBackend @@ -163,14 +160,11 @@ var ( _ resourcepb.ManagedObjectIndexServer = (*searchSupport)(nil) ) -func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.AccessClient, blob BlobSupport, tracer trace.Tracer, indexMetrics *BleveIndexMetrics, ownsIndexFn func(key NamespacedResource) (bool, error)) (support *searchSupport, err error) { +func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.AccessClient, blob BlobSupport, indexMetrics *BleveIndexMetrics, ownsIndexFn func(key NamespacedResource) (bool, error)) (support *searchSupport, err error) { // No backend search support if opts.Backend == nil { return nil, nil } - if tracer == nil { - return nil, fmt.Errorf("missing tracer") - } if opts.InitWorkerThreads < 1 { opts.InitWorkerThreads = 1 @@ -188,7 +182,6 @@ func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.A support = &searchSupport{ access: access, - tracer: tracer, storage: storage, search: opts.Backend, log: slog.Default().With("logger", "resource-search"), @@ -341,7 +334,7 @@ func (s *searchSupport) CountManagedObjects(ctx context.Context, req *resourcepb // Search implements ResourceIndexServer. func (s *searchSupport) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest) (*resourcepb.ResourceSearchResponse, error) { - ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Search") + ctx, span := tracer.Start(ctx, "resource.searchSupport.Search") defer span.End() if req.Options.Key.Namespace == "" || req.Options.Key.Group == "" || req.Options.Key.Resource == "" { @@ -499,7 +492,7 @@ func (s *searchSupport) buildIndexes(ctx context.Context) (int, error) { func (s *searchSupport) init(ctx context.Context) error { origCtx := ctx - ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Init") + ctx, span := tracer.Start(ctx, "resource.searchSupport.init") defer span.End() start := time.Now().Unix() @@ -632,7 +625,7 @@ func (s *searchSupport) runIndexRebuilder(ctx context.Context) { } func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) { - ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"RebuildIndex") + ctx, span := tracer.Start(ctx, "resource.searchSupport.rebuildIndex") defer span.End() l := s.log.With("namespace", req.Namespace, "group", req.Group, "resource", req.Resource) @@ -731,7 +724,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso return nil, fmt.Errorf("search is not configured properly (missing unifiedStorageSearch feature toggle?)") } - ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"GetOrCreateIndex") + ctx, span := tracer.Start(ctx, "resource.searchSupport.getOrCreateIndex") defer span.End() span.SetAttributes( attribute.String("namespace", key.Namespace), @@ -808,7 +801,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso } func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size int64, indexBuildReason string, rebuild bool) (ResourceIndex, error) { - ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Build") + ctx, span := tracer.Start(ctx, "resource.searchSupport.build") defer span.End() span.SetAttributes( diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index a35a8be7052..2c3cfe81097 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/authlib/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace/noop" dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" @@ -211,7 +210,7 @@ func TestSearchGetOrCreateIndex(t *testing.T) { InitMinCount: 1, // set min count to default for this test } - support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil, nil) + support, err := newSearchSupport(opts, storage, nil, nil, nil, nil) require.NoError(t, err) require.NotNil(t, support) @@ -267,7 +266,7 @@ func TestSearchGetOrCreateIndexWithIndexUpdate(t *testing.T) { } // Enable searchAfterWrite - support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil, nil) + support, err := newSearchSupport(opts, storage, nil, nil, nil, nil) require.NoError(t, err) require.NotNil(t, support) @@ -316,7 +315,7 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { InitMinCount: 1, // set min count to default for this test } - support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil, nil) + support, err := newSearchSupport(opts, storage, nil, nil, nil, nil) require.NoError(t, err) require.NotNil(t, support) @@ -594,7 +593,7 @@ func TestFindIndexesForRebuild(t *testing.T) { MinBuildVersion: semver.MustParse("5.5.5"), } - support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil, nil) + support, err := newSearchSupport(opts, storage, nil, nil, nil, nil) require.NoError(t, err) require.NotNil(t, support) @@ -665,7 +664,7 @@ func TestRebuildIndexes(t *testing.T) { Resources: supplier, } - support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil, nil) + support, err := newSearchSupport(opts, storage, nil, nil, nil, nil) require.NoError(t, err) require.NotNil(t, support) diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 59ae4e1dc83..13bac8a2f6e 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -14,8 +14,7 @@ import ( "github.com/Masterminds/semver" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" + "go.opentelemetry.io/otel" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -30,6 +29,8 @@ import ( "github.com/grafana/grafana/pkg/util/scheduler" ) +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/resource") + // ResourceServer implements all gRPC services type ResourceServer interface { resourcepb.ResourceStoreServer @@ -210,9 +211,6 @@ type SearchOptions struct { } type ResourceServerOptions struct { - // OTel tracer - Tracer trace.Tracer - // Real storage backend Backend StorageBackend @@ -259,10 +257,6 @@ type ResourceServerOptions struct { } func NewResourceServer(opts ResourceServerOptions) (*server, error) { - if opts.Tracer == nil { - opts.Tracer = noop.NewTracerProvider().Tracer("resource-server") - } - if opts.Backend == nil { return nil, fmt.Errorf("missing Backend implementation") } @@ -314,8 +308,8 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { } blobstore, err = NewCDKBlobSupport(ctx, CDKBlobSupportOptions{ - Tracer: opts.Tracer, - Bucket: NewInstrumentedBucket(bucket, opts.Reg, opts.Tracer), + Tracer: tracer, + Bucket: NewInstrumentedBucket(bucket, opts.Reg, tracer), }) if err != nil { return nil, err @@ -331,7 +325,6 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { // Make this cancelable ctx, cancel := context.WithCancel(context.Background()) s := &server{ - tracer: opts.Tracer, log: logger, backend: opts.Backend, blob: blobstore, @@ -355,7 +348,7 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { if opts.Search.Resources != nil { var err error - s.search, err = newSearchSupport(opts.Search, s.backend, s.access, s.blob, opts.Tracer, opts.IndexMetrics, opts.OwnsIndexFn) + s.search, err = newSearchSupport(opts.Search, s.backend, s.access, s.blob, opts.IndexMetrics, opts.OwnsIndexFn) if err != nil { return nil, err } @@ -373,7 +366,6 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { var _ ResourceServer = &server{} type server struct { - tracer trace.Tracer log *slog.Logger backend StorageBackend blob BlobSupport @@ -651,7 +643,7 @@ func (s *server) checkFolderMovePermissions(ctx context.Context, user claims.Aut } func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*resourcepb.CreateResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.Create") + ctx, span := tracer.Start(ctx, "resource.server.Create") defer span.End() if r := verifyRequestKey(req.Key); r != nil { @@ -681,7 +673,7 @@ func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*re }) } - s.sleepAfterSuccessfulWriteOperation(res, err) + s.sleepAfterSuccessfulWriteOperation("Create", req.Key, res, err) return res, err } @@ -714,7 +706,7 @@ type responseWithErrorResult interface { // Returns boolean indicating whether the sleep was performed or not (used in testing). // // This sleep is performed to guarantee search-after-write consistency, when rate-limiting updates to search index. -func (s *server) sleepAfterSuccessfulWriteOperation(res responseWithErrorResult, err error) bool { +func (s *server) sleepAfterSuccessfulWriteOperation(operation string, key *resourcepb.ResourceKey, res responseWithErrorResult, err error) bool { if s.artificialSuccessfulWriteDelay <= 0 { return false } @@ -733,12 +725,20 @@ func (s *server) sleepAfterSuccessfulWriteOperation(res responseWithErrorResult, } } + s.log.Debug("sleeping after successful write operation", + "operation", operation, + "delay", s.artificialSuccessfulWriteDelay, + "group", key.Group, + "resource", key.Resource, + "namespace", key.Namespace, + "name", key.Name) + time.Sleep(s.artificialSuccessfulWriteDelay) return true } func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*resourcepb.UpdateResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.Update") + ctx, span := tracer.Start(ctx, "resource.server.Update") defer span.End() rsp := &resourcepb.UpdateResponse{} @@ -768,7 +768,7 @@ func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*re }) } - s.sleepAfterSuccessfulWriteOperation(res, err) + s.sleepAfterSuccessfulWriteOperation("Update", req.Key, res, err) return res, err } @@ -812,7 +812,7 @@ func (s *server) update(ctx context.Context, user claims.AuthInfo, req *resource } func (s *server) Delete(ctx context.Context, req *resourcepb.DeleteRequest) (*resourcepb.DeleteResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.Delete") + ctx, span := tracer.Start(ctx, "resource.server.Delete") defer span.End() rsp := &resourcepb.DeleteResponse{} @@ -842,7 +842,7 @@ func (s *server) Delete(ctx context.Context, req *resourcepb.DeleteRequest) (*re }) } - s.sleepAfterSuccessfulWriteOperation(res, err) + s.sleepAfterSuccessfulWriteOperation("Delete", req.Key, res, err) return res, err } @@ -983,7 +983,7 @@ func (s *server) read(ctx context.Context, user claims.AuthInfo, req *resourcepb } func (s *server) List(ctx context.Context, req *resourcepb.ListRequest) (*resourcepb.ListResponse, error) { - ctx, span := s.tracer.Start(ctx, "storage_server.List") + ctx, span := tracer.Start(ctx, "resource.server.List") defer span.End() // The history + trash queries do not yet support additional filters @@ -1072,7 +1072,7 @@ func (s *server) List(ctx context.Context, req *resourcepb.ListRequest) (*resour pageBytes += len(item.Value) rsp.Items = append(rsp.Items, item) - if len(rsp.Items) >= int(req.Limit) || pageBytes >= maxPageBytes { + if (req.Limit > 0 && len(rsp.Items) >= int(req.Limit)) || pageBytes >= maxPageBytes { t := iter.ContinueToken() if iter.Next() { rsp.NextPageToken = t diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 66536b4d5d8..38df20e8151 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -4,20 +4,17 @@ import ( "context" "encoding/json" "errors" - "fmt" "log/slog" "net/http" - "os" "strings" "sync" "testing" "time" + badger "github.com/dgraph-io/badger/v4" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gocloud.dev/blob/fileblob" - "gocloud.dev/blob/memblob" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" authlib "github.com/grafana/authlib/types" @@ -41,20 +38,19 @@ func TestSimpleServer(t *testing.T) { } ctx := authlib.WithAuthInfo(context.Background(), testUserA) - bucket := memblob.OpenBucket(nil) - if false { - tmp, err := os.MkdirTemp("", "xxx-*") + // Create in-memory BadgerDB for testing + db, err := badger.Open(badger.DefaultOptions(""). + WithInMemory(true). + WithLogger(nil)) + require.NoError(t, err) + defer func() { + err := db.Close() require.NoError(t, err) + }() - bucket, err = fileblob.OpenBucket(tmp, &fileblob.Options{ - CreateDir: true, - Metadata: fileblob.MetadataDontWrite, // skip - }) - require.NoError(t, err) - fmt.Printf("ROOT: %s\n\n", tmp) - } - store, err := NewCDKBackend(ctx, CDKBackendOptions{ - Bucket: bucket, + kv := NewBadgerKV(db) + store, err := NewKVStorageBackend(KVBackendOptions{ + KvStore: kv, }) require.NoError(t, err) @@ -591,10 +587,13 @@ func newTestServerWithQueue(t *testing.T, maxSizePerTenant int, numWorkers int) } func TestArtificialDelayAfterSuccessfulOperation(t *testing.T) { - s := &server{artificialSuccessfulWriteDelay: 1 * time.Millisecond} + s := &server{ + artificialSuccessfulWriteDelay: 1 * time.Millisecond, + log: slog.Default(), + } check := func(t *testing.T, expectedSleep bool, res responseWithErrorResult, err error) { - slept := s.sleepAfterSuccessfulWriteOperation(res, err) + slept := s.sleepAfterSuccessfulWriteOperation("test", &resourcepb.ResourceKey{}, res, err) require.Equal(t, expectedSleep, slept) } diff --git a/pkg/storage/unified/resource/storage_backend.go b/pkg/storage/unified/resource/storage_backend.go index d6afaf43638..2ee08dd85d9 100644 --- a/pkg/storage/unified/resource/storage_backend.go +++ b/pkg/storage/unified/resource/storage_backend.go @@ -29,21 +29,41 @@ const ( prunerMaxEvents = 20 defaultEventRetentionPeriod = 1 * time.Hour defaultEventPruningInterval = 5 * time.Minute + clusterScopeNamespace = "__cluster__" ) +// convertClusterNamespaceToEmpty converts the internal __cluster__ namespace back to empty string +// for cluster-scoped resources when returning to users +func convertClusterNamespaceToEmpty(namespace string) string { + if namespace == clusterScopeNamespace { + return "" + } + return namespace +} + +// convertEmptyToClusterNamespace converts empty namespace to the internal __cluster__ namespace +// for cluster-scoped resources when WithExperimentalClusterScope is enabled +func convertEmptyToClusterNamespace(namespace string, withExperimentalClusterScope bool) string { + if withExperimentalClusterScope && namespace == "" { + return clusterScopeNamespace + } + return namespace +} + // kvStorageBackend Unified storage backend based on KV storage. type kvStorageBackend struct { - snowflake *snowflake.Node - kv KV - dataStore *dataStore - eventStore *eventStore - notifier *notifier - builder DocumentBuilder - log logging.Logger - withPruner bool - eventRetentionPeriod time.Duration - eventPruningInterval time.Duration - historyPruner Pruner + snowflake *snowflake.Node + kv KV + dataStore *dataStore + eventStore *eventStore + notifier *notifier + builder DocumentBuilder + log logging.Logger + withPruner bool + eventRetentionPeriod time.Duration + eventPruningInterval time.Duration + historyPruner Pruner + withExperimentalClusterScope bool //tracer trace.Tracer //reg prometheus.Registerer } @@ -51,12 +71,13 @@ type kvStorageBackend struct { var _ StorageBackend = &kvStorageBackend{} type KVBackendOptions struct { - KvStore KV - WithPruner bool - EventRetentionPeriod time.Duration // How long to keep events (default: 1 hour) - EventPruningInterval time.Duration // How often to run the event pruning (default: 5 minutes) - Tracer trace.Tracer // TODO add tracing - Reg prometheus.Registerer // TODO add metrics + KvStore KV + WithPruner bool + WithExperimentalClusterScope bool // Allow empty namespace to be used for cluster-scoped resources. + EventRetentionPeriod time.Duration // How long to keep events (default: 1 hour) + EventPruningInterval time.Duration // How often to run the event pruning (default: 5 minutes) + Tracer trace.Tracer // TODO add tracing + Reg prometheus.Registerer // TODO add metrics } func NewKVStorageBackend(opts KVBackendOptions) (StorageBackend, error) { @@ -80,15 +101,16 @@ func NewKVStorageBackend(opts KVBackendOptions) (StorageBackend, error) { } backend := &kvStorageBackend{ - kv: kv, - dataStore: newDataStore(kv), - eventStore: eventStore, - notifier: newNotifier(eventStore, notifierOptions{}), - snowflake: s, - builder: StandardDocumentBuilder(), // For now we use the standard document builder. - log: &logging.NoOpLogger{}, // Make this configurable - eventRetentionPeriod: eventRetentionPeriod, - eventPruningInterval: eventPruningInterval, + kv: kv, + dataStore: newDataStore(kv), + eventStore: eventStore, + notifier: newNotifier(eventStore, notifierOptions{}), + snowflake: s, + builder: StandardDocumentBuilder(), // For now we use the standard document builder. + log: &logging.NoOpLogger{}, // Make this configurable + eventRetentionPeriod: eventRetentionPeriod, + eventPruningInterval: eventPruningInterval, + withExperimentalClusterScope: opts.WithExperimentalClusterScope, } err = backend.initPruner(ctx) if err != nil { @@ -206,6 +228,8 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in } rv := k.snowflake.Generate().Int64() + namespace := convertEmptyToClusterNamespace(event.Key.Namespace, k.withExperimentalClusterScope) + obj := event.Object // Write data. var action DataAction @@ -216,7 +240,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in _, err := k.dataStore.GetLatestResourceKey(ctx, GetRequestKey{ Group: event.Key.Group, Resource: event.Key.Resource, - Namespace: event.Key.Namespace, + Namespace: namespace, Name: event.Key.Name, }) if err == nil { @@ -244,7 +268,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in err := k.dataStore.Save(ctx, DataKey{ Group: event.Key.Group, Resource: event.Key.Resource, - Namespace: event.Key.Namespace, + Namespace: namespace, Name: event.Key.Name, ResourceVersion: rv, Action: action, @@ -256,7 +280,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in // Write event err = k.eventStore.Save(ctx, Event{ - Namespace: event.Key.Namespace, + Namespace: namespace, Group: event.Key.Group, Resource: event.Key.Resource, Name: event.Key.Name, @@ -270,7 +294,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in } _ = k.historyPruner.Add(PruningKey{ - Namespace: event.Key.Namespace, + Namespace: namespace, Group: event.Key.Group, Resource: event.Key.Resource, Name: event.Key.Name, @@ -283,10 +307,46 @@ func (k *kvStorageBackend) ReadResource(ctx context.Context, req *resourcepb.Rea if req.Key == nil { return &BackendReadResponse{Error: &resourcepb.ErrorResult{Code: http.StatusBadRequest, Message: "missing key"}} } + + namespace := convertEmptyToClusterNamespace(req.Key.Namespace, k.withExperimentalClusterScope) + + // If a specific resource version is requested, validate that it's not too high + if req.ResourceVersion > 0 { + // Fetch the latest RV + latestRV := k.snowflake.Generate().Int64() + if lastEventKey, err := k.eventStore.LastEventKey(ctx); err == nil { + latestRV = lastEventKey.ResourceVersion + } else if !errors.Is(err, ErrNotFound) { + return &BackendReadResponse{Error: &resourcepb.ErrorResult{ + Code: http.StatusInternalServerError, + Message: fmt.Sprintf("failed to fetch latest resource version: %v", err), + }} + } + + // Check if the requested RV is higher than the latest available RV + if req.ResourceVersion > latestRV { + return &BackendReadResponse{ + Error: &resourcepb.ErrorResult{ + Code: http.StatusGatewayTimeout, + Reason: string(metav1.StatusReasonTimeout), // match etcd behavior + Message: "ResourceVersion is larger than max", + Details: &resourcepb.ErrorDetails{ + Causes: []*resourcepb.ErrorCause{ + { + Reason: string(metav1.CauseTypeResourceVersionTooLarge), + Message: fmt.Sprintf("requested: %d, current %d", req.ResourceVersion, latestRV), + }, + }, + }, + }, + } + } + } + meta, err := k.dataStore.GetResourceKeyAtRevision(ctx, GetRequestKey{ Group: req.Key.Group, Resource: req.Key.Resource, - Namespace: req.Key.Namespace, + Namespace: namespace, Name: req.Key.Name, }, req.ResourceVersion) if errors.Is(err, ErrNotFound) { @@ -297,7 +357,7 @@ func (k *kvStorageBackend) ReadResource(ctx context.Context, req *resourcepb.Rea data, err := k.dataStore.Get(ctx, DataKey{ Group: req.Key.Group, Resource: req.Key.Resource, - Namespace: req.Key.Namespace, + Namespace: namespace, Name: req.Key.Name, ResourceVersion: meta.ResourceVersion, Action: meta.Action, @@ -323,6 +383,9 @@ func (k *kvStorageBackend) ListIterator(ctx context.Context, req *resourcepb.Lis if req.Options == nil || req.Options.Key == nil { return 0, fmt.Errorf("missing options or key in ListRequest") } + + namespace := convertEmptyToClusterNamespace(req.Options.Key.Namespace, k.withExperimentalClusterScope) + // Parse continue token if provided offset := int64(0) resourceVersion := req.ResourceVersion @@ -335,8 +398,15 @@ func (k *kvStorageBackend) ListIterator(ctx context.Context, req *resourcepb.Lis resourceVersion = token.ResourceVersion } - // We set the listRV to the current time. + // We set the listRV to the last event resource version. + // If no events exist yet, we generate a new snowflake. listRV := k.snowflake.Generate().Int64() + if lastEventKey, err := k.eventStore.LastEventKey(ctx); err == nil { + listRV = lastEventKey.ResourceVersion + } else if !errors.Is(err, ErrNotFound) { + return 0, fmt.Errorf("failed to fetch last event: %w", err) + } + if resourceVersion > 0 { listRV = resourceVersion } @@ -347,7 +417,7 @@ func (k *kvStorageBackend) ListIterator(ctx context.Context, req *resourcepb.Lis for dataKey, err := range k.dataStore.ListResourceKeysAtRevision(ctx, ListRequestKey{ Group: req.Options.Key.Group, Resource: req.Options.Key.Resource, - Namespace: req.Options.Key.Namespace, + Namespace: namespace, Name: req.Options.Key.Name, }, resourceVersion) { if err != nil { @@ -360,7 +430,7 @@ func (k *kvStorageBackend) ListIterator(ctx context.Context, req *resourcepb.Lis } keys = append(keys, dataKey) // Only fetch the first limit items + 1 to get the next token. - if len(keys) >= int(req.Limit+1) { + if req.Limit > 0 && len(keys) >= int(req.Limit+1) { break } } @@ -439,7 +509,7 @@ func (i *kvListIterator) ResourceVersion() int64 { func (i *kvListIterator) Namespace() string { if i.currentDataObj != nil { - return i.currentDataObj.Key.Namespace + return convertClusterNamespaceToEmpty(i.currentDataObj.Key.Namespace) } return "" } @@ -705,7 +775,7 @@ func (k *kvStorageBackend) listModifiedSinceEventStore(ctx context.Context, key return func(yield func(*ModifiedResource, error) bool) { // store all events ordered by RV for the given tenant here eventKeys := make([]EventKey, 0) - for evtKeyStr, err := range k.eventStore.ListKeysSince(ctx, sinceRv-defaultLookbackPeriod.Nanoseconds()) { + for evtKeyStr, err := range k.eventStore.ListKeysSince(ctx, subtractDurationFromSnowflake(sinceRv, defaultLookbackPeriod)) { if err != nil { yield(&ModifiedResource{}, err) return @@ -737,14 +807,7 @@ func (k *kvStorageBackend) listModifiedSinceEventStore(ctx context.Context, key } seen[evtKey.Name] = struct{}{} - value, err := k.getValueFromDataStore(ctx, DataKey{ - Group: evtKey.Group, - Resource: evtKey.Resource, - Namespace: evtKey.Namespace, - Name: evtKey.Name, - ResourceVersion: evtKey.ResourceVersion, - Action: evtKey.Action, - }) + value, err := k.getValueFromDataStore(ctx, DataKey(evtKey)) if err != nil { yield(&ModifiedResource{}, err) return @@ -1056,7 +1119,7 @@ func (k *kvStorageBackend) WatchWriteEvents(ctx context.Context) (<-chan *Writte events <- &WrittenEvent{ Key: &resourcepb.ResourceKey{ - Namespace: event.Namespace, + Namespace: convertClusterNamespaceToEmpty(event.Namespace), Group: event.Group, Resource: event.Resource, Name: event.Name, diff --git a/pkg/storage/unified/resource/storage_backend_test.go b/pkg/storage/unified/resource/storage_backend_test.go index 8ec4bac7f71..85cf3331d7d 100644 --- a/pkg/storage/unified/resource/storage_backend_test.go +++ b/pkg/storage/unified/resource/storage_backend_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/bwmarrin/snowflake" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -37,6 +36,19 @@ func setupTestStorageBackend(t *testing.T) *kvStorageBackend { return kvBackend } +func setupTestStorageBackendWithClusterScope(t *testing.T) *kvStorageBackend { + kv := setupTestKV(t) + opts := KVBackendOptions{ + KvStore: kv, + WithPruner: true, + WithExperimentalClusterScope: true, + } + backend, err := NewKVStorageBackend(opts) + kvBackend := backend.(*kvStorageBackend) + require.NoError(t, err) + return kvBackend +} + func TestNewKvStorageBackend(t *testing.T) { backend := setupTestStorageBackend(t) @@ -324,6 +336,36 @@ func TestKvStorageBackend_ReadResource_DeletedResource(t *testing.T) { require.Equal(t, objectToJSONBytes(t, testObj), response.Value) } +func TestKvStorageBackend_ReadResource_TooHighResourceVersion(t *testing.T) { + backend := setupTestStorageBackend(t) + ctx := context.Background() + + // First, create a resource + _, rv := createAndWriteTestObject(t, backend) + + // Try to read with a resource version that's way too high + readReq := &resourcepb.ReadRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: "default", + Group: "apps", + Resource: "resources", + Name: "test-resource", + }, + ResourceVersion: rv + 1000000000000, // Way in the future + } + + response := backend.ReadResource(ctx, readReq) + require.NotNil(t, response.Error, "ReadResource should return error for too high resource version") + require.Equal(t, int32(504), response.Error.Code) // http.StatusGatewayTimeout + require.Equal(t, "Timeout", response.Error.Reason) + require.Equal(t, "ResourceVersion is larger than max", response.Error.Message) + require.NotNil(t, response.Error.Details) + require.Len(t, response.Error.Details.Causes, 1) + require.Equal(t, "ResourceVersionTooLarge", response.Error.Details.Causes[0].Reason) + require.Contains(t, response.Error.Details.Causes[0].Message, "requested:") + require.Contains(t, response.Error.Details.Causes[0].Message, "current") +} + func TestKvStorageBackend_ListIterator_Success(t *testing.T) { backend := setupTestStorageBackend(t) ctx := context.Background() @@ -495,21 +537,51 @@ func TestKvStorageBackend_ListIterator_WithPagination(t *testing.T) { var continueToken2 string _, err = backend.ListIterator(ctx, listReq, func(iter ListIterator) error { + count := 0 for iter.Next() { if err := iter.Error(); err != nil { return err } secondPageItems = append(secondPageItems, iter.Name()) + count++ + // Simulate pagination by getting continue token after limit items + if count >= int(listReq.Limit) { + continueToken2 = iter.ContinueToken() + break + } } - // Capture continue token for potential third page - continueToken2 = iter.ContinueToken() return iter.Error() }) - // TODO: fix the ListIterator to respect the limit. This require a change to the resource server. require.NoError(t, err) - require.Equal(t, 3, len(secondPageItems)) - require.Equal(t, []string{"resource-3", "resource-4", "resource-5"}, secondPageItems) + require.Equal(t, 2, len(secondPageItems)) + require.Equal(t, []string{"resource-3", "resource-4"}, secondPageItems) require.NotEmpty(t, continueToken2) + + // third page using continue token + listReq.NextPageToken = continueToken2 + var thirdPageItems []string + var continueToken3 string + + _, err = backend.ListIterator(ctx, listReq, func(iter ListIterator) error { + count := 0 + for iter.Next() { + if err := iter.Error(); err != nil { + return err + } + thirdPageItems = append(thirdPageItems, iter.Name()) + count++ + // Simulate pagination by getting continue token after limit items + if count >= int(listReq.Limit) { + continueToken = iter.ContinueToken() + break + } + } + return iter.Error() + }) + require.NoError(t, err) + require.Equal(t, 1, len(thirdPageItems)) + require.Equal(t, []string{"resource-5"}, thirdPageItems) + require.Empty(t, continueToken3) } func TestKvStorageBackend_ListIterator_EmptyResult(t *testing.T) { backend := setupTestStorageBackend(t) @@ -734,24 +806,10 @@ func randomStringGenerator() func() string { // creates 2 hour old snowflake for testing func generateOldSnowflake(t *testing.T) int64 { - // Generate a current snowflake first - node, err := snowflake.NewNode(1) - require.NoError(t, err) - currentSnowflake := node.Generate().Int64() - - // Extract its timestamp component by shifting right - currentTimestamp := currentSnowflake >> 22 - - // Subtract 2 hours (in milliseconds) from the timestamp - twoHoursMs := int64(2 * time.Hour / time.Millisecond) - oldTimestamp := currentTimestamp - twoHoursMs - - // Reconstruct snowflake: [timestamp:41][node:10][sequence:12] - // Keep the original node and sequence bits - nodeAndSequence := currentSnowflake & 0x3FFFFF // Bottom 22 bits (10 node + 12 sequence) - snowflakeID := (oldTimestamp << 22) | nodeAndSequence - - return snowflakeID + // Generate a snowflake for 2 hours ago using the snowflakeFromTime utility + // which properly handles the epoch + twoHoursAgo := time.Now().Add(-2 * time.Hour) + return snowflakeFromTime(twoHoursAgo) } // seedBackend seeds the kvstore with data and return the expected result for ListModifiedSince calls @@ -800,6 +858,111 @@ func seedBackend(t *testing.T, backend *kvStorageBackend, ctx context.Context, n return expectations } +func TestKvStorageBackend_ListModifiedSince_WithFolder(t *testing.T) { + backend := setupTestStorageBackend(t) + ctx := context.Background() + + // Use a unique resource type to avoid conflicts with other tests + ns := NamespacedResource{ + Namespace: "test-folder-ns", + Group: "test.folder.app", + Resource: "test-resources", + } + + // Create test objects with folder field set + testObj1, err := createTestObjectWithName("dashboard-1", ns, "data-1") + require.NoError(t, err) + metaAccessor1, err := utils.MetaAccessor(testObj1) + require.NoError(t, err) + metaAccessor1.SetFolder("folder-abc") + + testObj2, err := createTestObjectWithName("dashboard-2", ns, "data-2") + require.NoError(t, err) + metaAccessor2, err := utils.MetaAccessor(testObj2) + require.NoError(t, err) + metaAccessor2.SetFolder("folder-xyz") + + // Write first dashboard + writeEvent1 := WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: &resourcepb.ResourceKey{ + Namespace: ns.Namespace, + Group: ns.Group, + Resource: ns.Resource, + Name: "dashboard-1", + }, + Value: objectToJSONBytes(t, testObj1), + Object: metaAccessor1, + PreviousRV: 0, + } + rv1, err := backend.WriteEvent(ctx, writeEvent1) + require.NoError(t, err) + + // Write second dashboard + writeEvent2 := WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: &resourcepb.ResourceKey{ + Namespace: ns.Namespace, + Group: ns.Group, + Resource: ns.Resource, + Name: "dashboard-2", + }, + Value: objectToJSONBytes(t, testObj2), + Object: metaAccessor2, + PreviousRV: 0, + } + rv2, err := backend.WriteEvent(ctx, writeEvent2) + require.NoError(t, err) + + tests := []struct { + name string + sinceRV func() int64 + codePath string + }{ + { + name: "via event store (recent RV < 1 hour)", + sinceRV: func() int64 { return rv1 - 1 }, + codePath: "listModifiedSinceEventStore", + }, + { + name: "via data store (old RV > 1 hour)", + sinceRV: func() int64 { return generateOldSnowflake(t) }, + codePath: "listModifiedSinceDataStore", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sinceRV := tt.sinceRV() + + // List resources + rv, seq := backend.ListModifiedSince(ctx, ns, sinceRV) + + changes := make(map[string]*ModifiedResource) + for mr, err := range seq { + require.NoError(t, err, "Should not get error when Folder field is included") + require.Equal(t, ns.Group, mr.Key.Group) + require.Equal(t, ns.Namespace, mr.Key.Namespace) + require.Equal(t, ns.Resource, mr.Key.Resource) + changes[mr.Key.Name] = mr + } + + require.Greater(t, rv, sinceRV) + require.Len(t, changes, 2, "Should return 2 resources") + + // Verify dashboard-1 + require.Contains(t, changes, "dashboard-1") + require.Equal(t, rv1, changes["dashboard-1"].ResourceVersion) + require.Equal(t, objectToJSONBytes(t, testObj1), changes["dashboard-1"].Value) + + // Verify dashboard-2 + require.Contains(t, changes, "dashboard-2") + require.Equal(t, rv2, changes["dashboard-2"].ResourceVersion) + require.Equal(t, objectToJSONBytes(t, testObj2), changes["dashboard-2"].Value) + }) + } +} + func createAndSaveTestObject(t *testing.T, backend *kvStorageBackend, ctx context.Context, ns NamespacedResource, uniqueStringGen func() string, updates int, deleted bool) *ModifiedResource { name := uniqueStringGen() action := resourcepb.WatchEvent_ADDED @@ -1494,3 +1657,253 @@ func createAndWriteTestObject(t *testing.T, backend *kvStorageBackend) (*unstruc return testObj, rv } + +// TestKvStorageBackend_ClusterScopedResources tests create, update, delete, list, and watch +// operations for cluster-scoped resources (empty namespace). +// This test requires the backend to be configured with WithExperimentalClusterScoped set to true. +// +// The test verifies that: +// - All write operations accept empty namespace +// - ReadResource responses return empty namespace +// - ListIterator results return empty namespace +// - WatchWriteEvents return empty namespace +func TestKvStorageBackend_ClusterScopedResources(t *testing.T) { + backend := setupTestStorageBackendWithClusterScope(t) + ctx := context.Background() + + // Start watching for events before creating resources + stream, err := backend.WatchWriteEvents(ctx) + require.NoError(t, err) + + // Use empty namespace for cluster-scoped resources + clusterNS := NamespacedResource{ + Namespace: "", + Group: "cluster.example.com", + Resource: "clusterresources", + } + + // Test Create - Add 3 cluster-scoped resources + testObj1, err := createTestObjectWithName("cluster-item1", clusterNS, "data-1") + require.NoError(t, err) + metaAccessor1, err := utils.MetaAccessor(testObj1) + require.NoError(t, err) + + writeEvent1 := WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: &resourcepb.ResourceKey{ + Namespace: "", + Group: "cluster.example.com", + Resource: "clusterresources", + Name: "cluster-item1", + }, + Value: objectToJSONBytes(t, testObj1), + Object: metaAccessor1, + PreviousRV: 0, + } + rv1, err := backend.WriteEvent(ctx, writeEvent1) + require.NoError(t, err) + require.Greater(t, rv1, int64(0)) + + testObj2, err := createTestObjectWithName("cluster-item2", clusterNS, "data-2") + require.NoError(t, err) + metaAccessor2, err := utils.MetaAccessor(testObj2) + require.NoError(t, err) + + writeEvent2 := WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: &resourcepb.ResourceKey{ + Namespace: "", + Group: "cluster.example.com", + Resource: "clusterresources", + Name: "cluster-item2", + }, + Value: objectToJSONBytes(t, testObj2), + Object: metaAccessor2, + PreviousRV: 0, + } + rv2, err := backend.WriteEvent(ctx, writeEvent2) + require.NoError(t, err) + require.Greater(t, rv2, rv1) + + testObj3, err := createTestObjectWithName("cluster-item3", clusterNS, "data-3") + require.NoError(t, err) + metaAccessor3, err := utils.MetaAccessor(testObj3) + require.NoError(t, err) + + writeEvent3 := WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: &resourcepb.ResourceKey{ + Namespace: "", + Group: "cluster.example.com", + Resource: "clusterresources", + Name: "cluster-item3", + }, + Value: objectToJSONBytes(t, testObj3), + Object: metaAccessor3, + PreviousRV: 0, + } + rv3, err := backend.WriteEvent(ctx, writeEvent3) + require.NoError(t, err) + require.Greater(t, rv3, rv2) + + // Test Update - Modify cluster-item2 + testObj2.Object["spec"].(map[string]any)["value"] = "updated-data" + metaAccessor2Updated, err := utils.MetaAccessor(testObj2) + require.NoError(t, err) + + writeEvent2Updated := WriteEvent{ + Type: resourcepb.WatchEvent_MODIFIED, + Key: &resourcepb.ResourceKey{ + Namespace: "", + Group: "cluster.example.com", + Resource: "clusterresources", + Name: "cluster-item2", + }, + Value: objectToJSONBytes(t, testObj2), + Object: metaAccessor2Updated, + ObjectOld: metaAccessor2, + PreviousRV: rv2, + } + rv4, err := backend.WriteEvent(ctx, writeEvent2Updated) + require.NoError(t, err) + require.Greater(t, rv4, rv3) + + // Test Read - Read latest cluster-item2 + readReq := &resourcepb.ReadRequest{ + Key: &resourcepb.ResourceKey{ + Name: "cluster-item2", + Namespace: "", // Request with empty namespace + Group: "cluster.example.com", + Resource: "clusterresources", + }, + ResourceVersion: 0, + } + response := backend.ReadResource(ctx, readReq) + require.Nil(t, response.Error) + require.Equal(t, rv4, response.ResourceVersion) + require.Contains(t, string(response.Value), "updated-data") + require.NotNil(t, response.Key, "response key should be populated") + require.Empty(t, response.Key.Namespace, "cluster-scoped resource should have empty namespace in response") + + // Test Read - Read early version of cluster-item2 + readReq.ResourceVersion = rv3 // Should return rv2 version + response = backend.ReadResource(ctx, readReq) + require.Nil(t, response.Error) + require.Equal(t, rv2, response.ResourceVersion) + require.Contains(t, string(response.Value), "data-2") + require.NotNil(t, response.Key, "response key should be populated") + require.Empty(t, response.Key.Namespace, "cluster-scoped resource should have empty namespace in response") + + // Test Delete - Delete cluster-item1 + writeEvent1Delete := WriteEvent{ + Type: resourcepb.WatchEvent_DELETED, + Key: &resourcepb.ResourceKey{ + Namespace: "", + Group: "cluster.example.com", + Resource: "clusterresources", + Name: "cluster-item1", + }, + Value: objectToJSONBytes(t, testObj1), + Object: metaAccessor1, + ObjectOld: metaAccessor1, + PreviousRV: rv1, + } + rv5, err := backend.WriteEvent(ctx, writeEvent1Delete) + require.NoError(t, err) + require.Greater(t, rv5, rv4) + + // Test List - List all cluster-scoped resources + listReq := &resourcepb.ListRequest{ + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Namespace: "", + Group: "cluster.example.com", + Resource: "clusterresources", + }, + }, + Limit: 10, + } + + var listedItems []struct { + name string + namespace string + value []byte + } + rv, err := backend.ListIterator(ctx, listReq, func(iter ListIterator) error { + for iter.Next() { + if err := iter.Error(); err != nil { + return err + } + listedItems = append(listedItems, struct { + name string + namespace string + value []byte + }{ + name: iter.Name(), + namespace: iter.Namespace(), + value: iter.Value(), + }) + } + return iter.Error() + }) + + require.NoError(t, err) + require.GreaterOrEqual(t, rv, rv5) + require.Len(t, listedItems, 2) // cluster-item2 and cluster-item3 (item1 was deleted) + + // Verify all items have empty namespace + for _, item := range listedItems { + require.Empty(t, item.namespace, "cluster-scoped resources should have empty namespace") + } + + // Verify items are sorted and have expected content + require.Equal(t, "cluster-item2", listedItems[0].name) + require.Contains(t, string(listedItems[0].value), "updated-data") + require.Equal(t, "cluster-item3", listedItems[1].name) + require.Contains(t, string(listedItems[1].value), "data-3") + + // Verify deleted resource is not in list + readReqDeleted := &resourcepb.ReadRequest{ + Key: &resourcepb.ResourceKey{ + Name: "cluster-item1", + Namespace: "", + Group: "cluster.example.com", + Resource: "clusterresources", + }, + ResourceVersion: 0, + } + responseDeleted := backend.ReadResource(ctx, readReqDeleted) + require.NotNil(t, responseDeleted.Error) + require.Equal(t, int32(404), responseDeleted.Error.Code) + // Key should still be empty for cluster-scoped resources even on error + if responseDeleted.Key != nil { + require.Empty(t, responseDeleted.Key.Namespace, "cluster-scoped resource should have empty namespace even on error") + } + + // Test Watch - Verify all events were published with empty namespace + watchedEvents := []struct { + name string + expectedType resourcepb.WatchEvent_Type + expectedRV int64 + }{ + {"cluster-item1", resourcepb.WatchEvent_ADDED, rv1}, + {"cluster-item2", resourcepb.WatchEvent_ADDED, rv2}, + {"cluster-item3", resourcepb.WatchEvent_ADDED, rv3}, + {"cluster-item2", resourcepb.WatchEvent_MODIFIED, rv4}, + {"cluster-item1", resourcepb.WatchEvent_DELETED, rv5}, + } + + for i, expected := range watchedEvents { + select { + case event := <-stream: + require.Equal(t, expected.name, event.Key.Name, "Event %d: wrong name", i) + require.Empty(t, event.Key.Namespace, "Event %d: cluster-scoped resource should have empty namespace", i) + require.Equal(t, "cluster.example.com", event.Key.Group, "Event %d: wrong group", i) + require.Equal(t, "clusterresources", event.Key.Resource, "Event %d: wrong resource", i) + require.Equal(t, expected.expectedType, event.Type, "Event %d: wrong type", i) + require.Equal(t, expected.expectedRV, event.ResourceVersion, "Event %d: wrong resource version", i) + case <-ctx.Done(): + t.Fatalf("Timeout waiting for event %d", i) + } + } +} diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 7d81fbe19ee..9bc867ba0d3 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -40,7 +40,6 @@ import ( func TestMain(m *testing.M) { goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("github.com/open-feature/go-sdk/openfeature.(*eventExecutor).startEventListener.func1.1"), - goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"), goleak.IgnoreTopFunction("github.com/blevesearch/bleve_index_api.AnalysisWorker"), // These don't stop when index is closed. ) } diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index fbcb8b90b36..60fa9b8467a 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -62,7 +62,6 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) { } serverOptions := resource.ResourceServerOptions{ - Tracer: opts.Tracer, Blob: resource.BlobConfig{ URL: apiserverCfg.Key("blob_url").MustString(""), }, diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go index 8ce39d55e33..18be19d407e 100644 --- a/pkg/storage/unified/testing/storage_backend.go +++ b/pkg/storage/unified/testing/storage_backend.go @@ -387,7 +387,31 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend require.Empty(t, res.NextPageToken) }) - t.Run("list latest first page ", func(t *testing.T) { + t.Run("fetch all with limit 0", func(t *testing.T) { + res, err := server.List(ctx, &resourcepb.ListRequest{ + Limit: 0, + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Namespace: ns, + Group: "group", + Resource: "resource", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 5) + // should be sorted by key ASC + require.Contains(t, string(res.Items[0].Value), "item1 ADDED") + require.Contains(t, string(res.Items[1].Value), "item2 MODIFIED") + require.Contains(t, string(res.Items[2].Value), "item4 ADDED") + require.Contains(t, string(res.Items[3].Value), "item5 ADDED") + require.Contains(t, string(res.Items[4].Value), "item6 ADDED") + + require.Empty(t, res.NextPageToken) + }) + + t.Run("fetch all latest with pagination", func(t *testing.T) { res, err := server.List(ctx, &resourcepb.ListRequest{ Limit: 3, Options: &resourcepb.ListOptions{ @@ -407,6 +431,22 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend require.Contains(t, string(res.Items[1].Value), "item2 MODIFIED") require.Contains(t, string(res.Items[2].Value), "item4 ADDED") require.GreaterOrEqual(t, continueToken.ResourceVersion, rv8) + + res, err = server.List(ctx, &resourcepb.ListRequest{ + Limit: 3, + NextPageToken: continueToken.String(), + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Namespace: ns, + Group: "group", + Resource: "resource", + }, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 2) + require.Empty(t, res.NextPageToken) }) t.Run("list at revision", func(t *testing.T) { @@ -430,7 +470,7 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend require.Empty(t, res.NextPageToken) }) - t.Run("fetch first page at revision with limit", func(t *testing.T) { + t.Run("list at revision with pagination", func(t *testing.T) { res, err := server.List(ctx, &resourcepb.ListRequest{ Limit: 3, ResourceVersion: rv7, @@ -446,7 +486,6 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend require.NoError(t, err) require.Nil(t, res.Error) require.Len(t, res.Items, 3) - t.Log(res.Items) require.Contains(t, string(res.Items[0].Value), "item1 ADDED") require.Contains(t, string(res.Items[1].Value), "item2 MODIFIED") require.Contains(t, string(res.Items[2].Value), "item4 ADDED") @@ -454,17 +493,11 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend continueToken, err := resource.GetContinueToken(res.NextPageToken) require.NoError(t, err) require.Equal(t, rv7, continueToken.ResourceVersion) - }) - t.Run("fetch second page at revision", func(t *testing.T) { - continueToken := &resource.ContinueToken{ - ResourceVersion: rv8, - StartOffset: 2, - SortAscending: false, - } - res, err := server.List(ctx, &resourcepb.ListRequest{ - NextPageToken: continueToken.String(), - Limit: 2, + res, err = server.List(ctx, &resourcepb.ListRequest{ + Limit: 3, + ResourceVersion: rv7, + NextPageToken: continueToken.String(), Options: &resourcepb.ListOptions{ Key: &resourcepb.ResourceKey{ Namespace: ns, @@ -474,16 +507,11 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend }, }) require.NoError(t, err) - require.Nil(t, res.Error) - require.Len(t, res.Items, 2) - t.Log(res.Items) - require.Contains(t, string(res.Items[0].Value), "item4 ADDED") - require.Contains(t, string(res.Items[1].Value), "item5 ADDED") - - continueToken, err = resource.GetContinueToken(res.NextPageToken) require.NoError(t, err) - require.Equal(t, rv8, continueToken.ResourceVersion) - require.Equal(t, int64(4), continueToken.StartOffset) + require.Nil(t, res.Error) + require.Len(t, res.Items, 1) + require.Contains(t, string(res.Items[0].Value), "item5 ADDED") + require.Empty(t, res.NextPageToken) }) } @@ -757,6 +785,30 @@ func runTestIntegrationBackendListHistory(t *testing.T, backend resource.Storage require.Contains(t, string(secondPageRes.Items[i].Value), "item1 MODIFIED") } }) + + // Test with limit=0 (should return all items) + t.Run("fetch all history with limit 0", func(t *testing.T) { + res, err := server.List(ctx, &resourcepb.ListRequest{ + Limit: 0, + Source: resourcepb.ListRequest_HISTORY, + Options: &resourcepb.ListOptions{ + Key: baseKey, + }, + }) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 6) // Should return all 6 history items (1 ADDED + 5 MODIFIED) + + // Should be in descending order (default for history) + require.Equal(t, rvHistory5, res.Items[0].ResourceVersion) + require.Equal(t, rvHistory4, res.Items[1].ResourceVersion) + require.Equal(t, rvHistory3, res.Items[2].ResourceVersion) + require.Equal(t, rvHistory2, res.Items[3].ResourceVersion) + require.Equal(t, rvHistory1, res.Items[4].ResourceVersion) + require.Equal(t, rv1, res.Items[5].ResourceVersion) + + require.Empty(t, res.NextPageToken) + }) }) t.Run("fetch second page of history at revision", func(t *testing.T) { diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 7c080b84505..50ab7d939ea 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -1211,5 +1211,137 @@ func TestIntegrationDashboardServicePermissions(t *testing.T) { assert.Contains(t, foundTitles, "dashboard in parent", "Should return dashboard in parent folder") assert.NotContains(t, foundTitles, "dashboard in child", "Should not return dashboard in child folder") }) + + t.Run("user with edit permissions on dashboard but not on parent folder should be able to edit the dashboard", func(t *testing.T) { + testFolder := createFolder(t, grafanaListedAddr, "restricted folder") + testDash := createDashboard(t, grafanaListedAddr, "dashboard with specific permissions", testFolder.ID, testFolder.UID) // nolint:staticcheck + restrictedUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleNone), + Login: "restricteduser", + Password: "restricteduser", + IsAdmin: false, + }) + setDashboardPermissions := func(t *testing.T, grafanaListedAddr string, dashboardUID string, permissions []map[string]interface{}) { + t.Helper() + + permissionPayload := map[string]interface{}{ + "items": permissions, + } + + payloadBytes, err := json.Marshal(permissionPayload) + require.NoError(t, err) + + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/uid/%s/permissions", grafanaListedAddr, dashboardUID) + req, err := http.NewRequest(http.MethodPost, u, bytes.NewBuffer(payloadBytes)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + require.NoError(t, err) + } + editPermissions := []map[string]interface{}{ + { + "permission": 2, + "userId": restrictedUserID, + }, + } + setDashboardPermissions(t, grafanaListedAddr, testDash.UID, editPermissions) + + // user cannot access the folder + u := fmt.Sprintf("http://restricteduser:restricteduser@%s/api/folders/%s", grafanaListedAddr, testFolder.UID) + resp, err := http.Get(u) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode, "User should not have access to the folder") + err = resp.Body.Close() + require.NoError(t, err) + + // but can edit the dashboard + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": testDash.UID, + "title": "Updated title by restricted user", + }, + "folderUid": testFolder.UID, + "overwrite": true, + } + resp, err = postDashboard(t, grafanaListedAddr, "restricteduser", "restricteduser", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode, "User should be able to edit dashboard even without folder access") + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("user with no permissions on dashboard or parent folder should not be able to edit the dashboard", func(t *testing.T) { + testFolder := createFolder(t, grafanaListedAddr, "restricted folder") + testDash := createDashboard(t, grafanaListedAddr, "dashboard with specific permissions", testFolder.ID, testFolder.UID) // nolint:staticcheck + + // user cannot access the folder + u := fmt.Sprintf("http://restricteduser:restricteduser@%s/api/folders/%s", grafanaListedAddr, testFolder.UID) + resp, err := http.Get(u) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode, "User should not have access to the folder") + err = resp.Body.Close() + require.NoError(t, err) + + // and cannot edit the dashboard + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": testDash.UID, + "title": "Updated title by restricted user", + }, + "folderUid": testFolder.UID, + "overwrite": true, + } + resp, err = postDashboard(t, grafanaListedAddr, "restricteduser", "restricteduser", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode, "User should not be able to edit dashboard without any permissions") + err = resp.Body.Close() + require.NoError(t, err) + }) + + t.Run("user with edit permissions on parent folder should be able to edit dashboard through permission inheritance", func(t *testing.T) { + inheritedPermissionsUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleNone), + Login: "inheriteduser", + Password: "inheriteduser", + IsAdmin: false, + }) + testFolder := createFolder(t, grafanaListedAddr, "folder with inherited permissions") + editFolderPermissions := []map[string]interface{}{ + { + "permission": 2, + "userId": inheritedPermissionsUserID, + }, + } + setFolderPermissions(t, grafanaListedAddr, testFolder.UID, editFolderPermissions) + + // create dashboard in the folder without specific dashboard permissions + testDash := createDashboard(t, grafanaListedAddr, "dashboard inheriting permissions", testFolder.ID, testFolder.UID) // nolint:staticcheck + u := fmt.Sprintf("http://inheriteduser:inheriteduser@%s/api/folders/%s", grafanaListedAddr, testFolder.UID) + resp, err := http.Get(u) // nolint:gosec + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode, "User should have access to the folder") + err = resp.Body.Close() + require.NoError(t, err) + + // and can edit the dashboard by inheriting permissions from the folder + dashboardPayload := map[string]interface{}{ + "dashboard": map[string]interface{}{ + "uid": testDash.UID, + "title": "Updated title via inherited permissions", + }, + "folderUid": testFolder.UID, + "overwrite": true, + } + resp, err = postDashboard(t, grafanaListedAddr, "inheriteduser", "inheriteduser", dashboardPayload) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode, "User should be able to edit dashboard through inherited folder permissions") + err = resp.Body.Close() + require.NoError(t, err) + }) }) } diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 78d56e06675..24f705eccd1 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -1580,7 +1580,7 @@ "keywords": null }, "dependencies": { - "grafanaDependency": "", + "grafanaDependency": "\u003e=10.3.0-0", "grafanaVersion": "*", "plugins": [], "extensions": { diff --git a/pkg/tests/apis/alerting/notifications/common/testing.go b/pkg/tests/apis/alerting/notifications/common/testing.go index bd9891402c9..2376b1368aa 100644 --- a/pkg/tests/apis/alerting/notifications/common/testing.go +++ b/pkg/tests/apis/alerting/notifications/common/testing.go @@ -3,12 +3,11 @@ package common import ( "testing" - "github.com/grafana/grafana/pkg/tests/apis" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" + "github.com/grafana/grafana/pkg/tests/apis" ) func NewReceiverClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList] { @@ -19,11 +18,8 @@ func NewReceiverClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1. return &apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList]{ Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.ReceiverKind().Group(), - Version: v0alpha1.ReceiverKind().Version(), - Resource: v0alpha1.ReceiverKind().Plural(), - }).Namespace("default"), + v0alpha1.ReceiverKind().GroupVersionResource()). + Namespace("default"), } } @@ -35,11 +31,7 @@ func NewRoutingTreeClient(t *testing.T, user apis.User) *apis.TypedClient[v0alph return &apis.TypedClient[v0alpha1.RoutingTree, v0alpha1.RoutingTreeList]{ Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.RoutingTreeKind().Group(), - Version: v0alpha1.RoutingTreeKind().Version(), - Resource: v0alpha1.RoutingTreeKind().Plural(), - }).Namespace("default"), + v0alpha1.RoutingTreeKind().GroupVersionResource()).Namespace("default"), } } @@ -51,11 +43,7 @@ func NewTemplateGroupClient(t *testing.T, user apis.User) *apis.TypedClient[v0al return &apis.TypedClient[v0alpha1.TemplateGroup, v0alpha1.TemplateGroupList]{ Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.TemplateGroupKind().Group(), - Version: v0alpha1.TemplateGroupKind().Version(), - Resource: v0alpha1.TemplateGroupKind().Plural(), - }).Namespace("default"), + v0alpha1.TemplateGroupKind().GroupVersionResource()).Namespace("default"), } } @@ -67,10 +55,6 @@ func NewTimeIntervalClient(t *testing.T, user apis.User) *apis.TypedClient[v0alp return &apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList]{ Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.TimeIntervalKind().Group(), - Version: v0alpha1.TimeIntervalKind().Version(), - Resource: v0alpha1.TimeIntervalKind().Plural(), - }).Namespace("default"), + v0alpha1.TimeIntervalKind().GroupVersionResource()).Namespace("default"), } } diff --git a/pkg/tests/apis/alerting/rules/alertrule/alertrule_test.go b/pkg/tests/apis/alerting/rules/alertrule/alertrule_test.go index ef44d030360..20b19346a3d 100644 --- a/pkg/tests/apis/alerting/rules/alertrule/alertrule_test.go +++ b/pkg/tests/apis/alerting/rules/alertrule/alertrule_test.go @@ -461,7 +461,7 @@ func TestIntegrationCRUD(t *testing.T) { } created, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{}) - require.ErrorContains(t, err, "invalid alert rule") + require.ErrorContains(t, err, "trigger interval must be a multiple of base evaluation interval") require.Nil(t, created) }) } @@ -564,3 +564,148 @@ func TestIntegrationBasicAPI(t *testing.T) { t.Logf("Got error: %s", err) }) } + +func TestIntegrationFolderLabelSyncAndValidation(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + ctx := context.Background() + helper := common.GetTestHelper(t) + client := common.NewAlertRuleClient(t, helper.Org1.Admin) + + // Prepare two folders for label sync update scenario + common.CreateTestFolder(t, helper, "test-folder-a") + common.CreateTestFolder(t, helper, "test-folder-b") + + baseGen := ngmodels.RuleGen.With( + ngmodels.RuleMuts.WithUniqueUID(), + ngmodels.RuleMuts.WithUniqueTitle(), + ngmodels.RuleMuts.WithNamespaceUID("test-folder-a"), + ngmodels.RuleMuts.WithGroupName("test-group"), + ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second), + ) + + t.Run("should keep folder label in sync with folder annotation on create and update", func(t *testing.T) { + rule := baseGen.Generate() + + alertRule := &v0alpha1.AlertRule{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + v0alpha1.FolderAnnotationKey: "test-folder-a", + }, + }, + Spec: v0alpha1.AlertRuleSpec{ + Title: rule.Title, + Expressions: v0alpha1.AlertRuleExpressionMap{ + "A": { + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), + Model: rule.Data[0].Model, + Source: util.Pointer(true), + RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ + From: v0alpha1.AlertRulePromDurationWMillis("5m"), + To: v0alpha1.AlertRulePromDurationWMillis("0s"), + }, + }, + }, + Trigger: v0alpha1.AlertRuleIntervalTrigger{ + Interval: v0alpha1.AlertRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)), + }, + NoDataState: string(rule.NoDataState), + ExecErrState: string(rule.ExecErrState), + }, + } + + created, err := client.Create(ctx, alertRule, v1.CreateOptions{}) + require.NoError(t, err) + defer func() { _ = client.Delete(ctx, created.Name, v1.DeleteOptions{}) }() + + // On create, metadata.labels[v0alpha1.FolderLabelKey] should mirror annotation + require.Equal(t, "test-folder-a", created.Labels[v0alpha1.FolderLabelKey]) + + // Update annotation to point to a different folder and ensure label follows + updated := created.Copy().(*v0alpha1.AlertRule) + if updated.Annotations == nil { + updated.Annotations = map[string]string{} + } + updated.Annotations[v0alpha1.FolderAnnotationKey] = "test-folder-b" + + after, err := client.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.Equal(t, "test-folder-b", after.Annotations[v0alpha1.FolderAnnotationKey]) + require.Equal(t, "test-folder-b", after.Labels[v0alpha1.FolderLabelKey]) + }) + + t.Run("should fail to create rule without folder annotation", func(t *testing.T) { + rule := baseGen.Generate() + + alertRule := &v0alpha1.AlertRule{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{}, // missing grafana.app/folder + }, + Spec: v0alpha1.AlertRuleSpec{ + Title: rule.Title, + Expressions: v0alpha1.AlertRuleExpressionMap{ + "A": { + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), + Model: rule.Data[0].Model, + Source: util.Pointer(true), + RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ + From: v0alpha1.AlertRulePromDurationWMillis("5m"), + To: v0alpha1.AlertRulePromDurationWMillis("0s"), + }, + }, + }, + Trigger: v0alpha1.AlertRuleIntervalTrigger{ + Interval: v0alpha1.AlertRulePromDuration("10s"), + }, + NoDataState: "NoData", + ExecErrState: "Error", + }, + } + + created, err := client.Create(ctx, alertRule, v1.CreateOptions{}) + require.Error(t, err) + require.Nil(t, created) + }) + + t.Run("should fail to create rule with group labels preset", func(t *testing.T) { + rule := baseGen.Generate() + alertRule := &v0alpha1.AlertRule{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + v0alpha1.FolderAnnotationKey: "test-folder-a", + }, + Labels: map[string]string{ + v0alpha1.GroupLabelKey: "some-group", + v0alpha1.GroupIndexLabelKey: "0", + }, + }, + Spec: v0alpha1.AlertRuleSpec{ + Title: rule.Title, + Expressions: v0alpha1.AlertRuleExpressionMap{ + "A": { + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)), + Model: rule.Data[0].Model, + Source: util.Pointer(true), + RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{ + From: v0alpha1.AlertRulePromDurationWMillis("5m"), + To: v0alpha1.AlertRulePromDurationWMillis("0s"), + }, + }, + }, + Trigger: v0alpha1.AlertRuleIntervalTrigger{Interval: v0alpha1.AlertRulePromDuration("10s")}, + NoDataState: "NoData", + ExecErrState: "Error", + }, + } + + created, err := client.Create(ctx, alertRule, v1.CreateOptions{}) + require.Error(t, err) + require.Nil(t, created) + }) +} diff --git a/pkg/tests/apis/alerting/rules/common/testing.go b/pkg/tests/apis/alerting/rules/common/testing.go index da9dd55a694..be43be5da26 100644 --- a/pkg/tests/apis/alerting/rules/common/testing.go +++ b/pkg/tests/apis/alerting/rules/common/testing.go @@ -4,14 +4,14 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" + "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/stretchr/testify/require" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" ) func NewAlertRuleClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.AlertRule, v0alpha1.AlertRuleList] { @@ -22,11 +22,7 @@ func NewAlertRuleClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1 return &apis.TypedClient[v0alpha1.AlertRule, v0alpha1.AlertRuleList]{ Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.AlertRuleKind().Group(), - Version: v0alpha1.AlertRuleKind().Version(), - Resource: v0alpha1.AlertRuleKind().Plural(), - }).Namespace("default"), + v0alpha1.AlertRuleKind().GroupVersionResource()).Namespace("default"), } } @@ -38,11 +34,7 @@ func NewRecordingRuleClient(t *testing.T, user apis.User) *apis.TypedClient[v0al return &apis.TypedClient[v0alpha1.RecordingRule, v0alpha1.RecordingRuleList]{ Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.RecordingRuleKind().Group(), - Version: v0alpha1.RecordingRuleKind().Version(), - Resource: v0alpha1.RecordingRuleKind().Plural(), - }).Namespace("default"), + v0alpha1.RecordingRuleKind().GroupVersionResource()).Namespace("default"), } } @@ -54,11 +46,7 @@ func NewFolderClient(t *testing.T, user apis.User) *apis.TypedClient[folders.Fol return &apis.TypedClient[folders.Folder, folders.FolderList]{ Client: client.Resource( - schema.GroupVersionResource{ - Group: folders.FolderKind().Group(), - Version: folders.FolderKind().Version(), - Resource: folders.FolderKind().Plural(), - }).Namespace("default"), + folders.FolderKind().GroupVersionResource()).Namespace("default"), } } diff --git a/pkg/tests/apis/alerting/rules/compat/convertprometheus_retrieval_test.go b/pkg/tests/apis/alerting/rules/compat/convertprometheus_retrieval_test.go new file mode 100644 index 00000000000..21a89180209 --- /dev/null +++ b/pkg/tests/apis/alerting/rules/compat/convertprometheus_retrieval_test.go @@ -0,0 +1,227 @@ +package compat + +import ( + "context" + "encoding/json" + "testing" + "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + prom_model "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/tests/api/alerting" + "github.com/grafana/grafana/pkg/tests/apis/alerting/rules/common" + "github.com/grafana/grafana/pkg/util/testutil" +) + +// TestIntegrationConvertPrometheusAlertRuleRetrieval verifies that an alert rule created +// through the Prometheus (mimirtool compatible) conversion API can be retrieved via the +// new Kubernetes rules API and retains expected fields (title, expressions, interval, +// folder annotation, group labels, and provenance converted_prometheus). +func TestIntegrationConvertPrometheusAlertRuleRetrieval(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + ctx := context.Background() + helper := common.GetTestHelper(t) + + // K8s client for new rules API + k8sAlertClient := common.NewAlertRuleClient(t, helper.Org1.Admin) + + // Legacy + conversion API client + legacyClient := alerting.NewAlertingLegacyAPIClient(helper.GetListenerAddress(), "admin", "admin") + + // Ensure legacy API is enabled (sanity) + allRules, status, _ := legacyClient.GetAllRulesWithStatus(t) + require.Equal(t, 200, status) + require.NotNil(t, allRules) + + // Create folder that will act as namespace title + folderUID := "test-folder-convert" + common.CreateTestFolder(t, helper, folderUID) + + // Build a Prometheus-compatible rule group payload (minimal) for conversion API + // We simulate a simple alert rule with expr and for duration; conversion API will set provenance. + // We pick static values; Grafana rule title will mirror Prometheus 'alert' name. + forDuration := prom_model.Duration(10 * time.Second) + interval20, err := prom_model.ParseDuration("20s") + require.NoError(t, err) + promGroup := apimodels.PrometheusRuleGroup{ + Name: "test-group", + Interval: interval20, + Rules: []apimodels.PrometheusRule{ + { + Alert: "ConvertedAlertTest", + Expr: "vector(1)", // simple always firing expression + For: &forDuration, + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{ + "summary": "Converted alert rule test", + }, + }, + }, + } + + // Create a real Prometheus datasource; conversion API requires a Prometheus-compatible datasource (cannot use __expr__). + ds := legacyClient.CreateDatasource(t, "prometheus") + dsUID := ds.Body.Datasource.UID + require.NotEmpty(t, dsUID, "prometheus datasource UID must not be empty") + defer legacyClient.DeleteDatasource(t, dsUID) + headers := map[string]string{} + + // Post conversion (client signature: namespaceTitle, datasourceUID, promGroup, headers) + // Use the folder title as the namespace title for conversion API lookup. + // Our helper created a folder with title "Test Folder" and UID folderUID. + resp := legacyClient.ConvertPrometheusPostRuleGroup(t, "Test Folder", dsUID, promGroup, headers) + require.Equal(t, "success", resp.Status) + + // Retrieve the converted rule via new K8s API immediately after the API call. + ruleList, err := k8sAlertClient.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + var found *v0alpha1.AlertRule + for _, item := range ruleList.Items { + if item.Spec.Title == "ConvertedAlertTest" { // Title should match the Prometheus alert name + copy := item.DeepCopy() + found = copy + break + } + } + require.NotNil(t, found, "expected to find converted alert rule via K8s API") + + // Assertions on converted rule fields + require.Equal(t, folderUID, found.Annotations["grafana.app/folder"], "folder annotation must match folder UID") + require.Equal(t, "test-group", found.Labels[v0alpha1.GroupLabelKey], "group label must match group name") + require.NotEmpty(t, found.Labels[v0alpha1.GroupIndexLabelKey], "group index label must be populated") + + // Interval: parse prom group interval and compare with spec trigger interval + require.Equal(t, promGroup.Interval.String(), string(found.Spec.Trigger.Interval), "interval mismatch") + + // Expression/model checks: Alert rule conversion produces query + math + threshold nodes. + require.Equal(t, 3, len(found.Spec.Expressions), "expected three expressions (query, math, threshold) in converted alert rule") + for ref, exp := range found.Spec.Expressions { + require.NotNil(t, exp.Model, "expression model %s should not be nil", ref) + // Only query expression carries a datasource UID; math/threshold may omit it. If present, must match dsUID. + if exp.DatasourceUID != nil { + require.EqualValues(t, dsUID, *exp.DatasourceUID) + } + } + + // Provenance should be converted_prometheus + require.Equal(t, ngmodels.ProvenanceConvertedPrometheus, ngmodels.Provenance(found.GetProvenanceStatus()), "provenance mismatch") + + // Basic JSON model sanity (non-empty if map) + // Basic JSON model sanity for at least one expression + var sanityChecked bool + for _, exp := range found.Spec.Expressions { + if m, ok := exp.Model.(map[string]interface{}); ok { + require.NotEmpty(t, m) + sanityChecked = true + break + } + } + require.True(t, sanityChecked, "expected at least one expression model to be a non-empty map") +} + +// TestIntegrationConvertPrometheusRecordingRuleRetrieval verifies recording rule conversion retrieval via K8s API. +func TestIntegrationConvertPrometheusRecordingRuleRetrieval(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + ctx := context.Background() + helper := common.GetTestHelper(t) + + k8sRecordingClient := common.NewRecordingRuleClient(t, helper.Org1.Admin) + legacyClient := alerting.NewAlertingLegacyAPIClient(helper.GetListenerAddress(), "admin", "admin") + + allRules, status, _ := legacyClient.GetAllRulesWithStatus(t) + require.Equal(t, 200, status) + require.NotNil(t, allRules) + + folderUID := "test-folder-convert-record" + common.CreateTestFolder(t, helper, folderUID) + + forDuration := prom_model.Duration(5 * time.Second) + interval20, err := prom_model.ParseDuration("20s") + require.NoError(t, err) + promGroup := apimodels.PrometheusRuleGroup{ + Name: "test-group-rec", + Interval: interval20, + Rules: []apimodels.PrometheusRule{ + { + Record: "converted_metric_total", + Expr: "vector(2)", + For: &forDuration, // For is ignored for recording rules but included for consistency + Labels: map[string]string{"job": "demo"}, + Annotations: map[string]string{"summary": "Converted recording rule test"}, + }, + }, + } + // Create a real Prometheus datasource for recording rule conversion. + ds := legacyClient.CreateDatasource(t, "prometheus") + dsUID := ds.Body.Datasource.UID + require.NotEmpty(t, dsUID, "prometheus datasource UID must not be empty") + defer legacyClient.DeleteDatasource(t, dsUID) + headers := map[string]string{ + "X-Grafana-Alerting-Target-Datasource-UID": dsUID, + } + + resp := legacyClient.ConvertPrometheusPostRuleGroup(t, "Test Folder", dsUID, promGroup, headers) + require.Equal(t, "success", resp.Status) + + // Retrieve the converted recording rule immediately + list, err := k8sRecordingClient.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + var found *v0alpha1.RecordingRule + for _, item := range list.Items { + if item.Spec.Metric == "converted_metric_total" { + copy := item.DeepCopy() + found = copy + break + } + } + require.NotNil(t, found, "expected to find converted recording rule via K8s API") + + require.Equal(t, folderUID, found.Annotations["grafana.app/folder"], "folder annotation must match") + require.Equal(t, "test-group-rec", found.Labels[v0alpha1.GroupLabelKey]) + require.NotEmpty(t, found.Labels[v0alpha1.GroupIndexLabelKey]) + + require.Equal(t, promGroup.Interval.String(), string(found.Spec.Trigger.Interval)) + + // Verify expressions map non-empty + require.Equal(t, 1, len(found.Spec.Expressions)) + var exprSpec v0alpha1.RecordingRuleExpression + for _, v := range found.Spec.Expressions { + exprSpec = v + break + } + require.NotNil(t, exprSpec.Model) + + // Datasource should match provided header + require.EqualValues(t, dsUID, *exprSpec.DatasourceUID) + + require.Equal(t, ngmodels.ProvenanceConvertedPrometheus, ngmodels.Provenance(found.GetProvenanceStatus())) + + if m, ok := exprSpec.Model.(map[string]interface{}); ok { + require.NotEmpty(t, m) + } else if b, ok := exprSpec.Model.([]byte); ok { + tmp := map[string]interface{}{} + _ = json.Unmarshal(b, &tmp) + require.NotEmpty(t, tmp) + } +} + +// If this test fails, it indicates that the accepted provenance statuses +// in v0alpha1/ext.go are out of sync with the known provenances in ngalert models. +// Make sure to keep them in sync. +func TestProvenancesMatch(t *testing.T) { + modelProvenances := make([]string, 0, len(ngmodels.KnownProvenances)) + for _, p := range ngmodels.KnownProvenances { + modelProvenances = append(modelProvenances, string(p)) + } + require.ElementsMatch(t, v0alpha1.AcceptedProvenanceStatuses, modelProvenances) +} diff --git a/pkg/tests/apis/alerting/rules/recordingrule/recordingrule_test.go b/pkg/tests/apis/alerting/rules/recordingrule/recordingrule_test.go index 783a90f777d..e1c586f9bb9 100644 --- a/pkg/tests/apis/alerting/rules/recordingrule/recordingrule_test.go +++ b/pkg/tests/apis/alerting/rules/recordingrule/recordingrule_test.go @@ -454,7 +454,7 @@ func TestIntegrationCRUD(t *testing.T) { } created, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{}) - require.ErrorContains(t, err, "invalid alert rule") + require.ErrorContains(t, err, "trigger interval must be a multiple of base evaluation interval") require.Nil(t, created) }) } @@ -557,3 +557,139 @@ func TestIntegrationBasicAPI(t *testing.T) { t.Logf("Got error: %s", err) }) } + +func TestIntegrationFolderLabelSyncAndValidation(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + ctx := context.Background() + helper := common.GetTestHelper(t) + client := common.NewRecordingRuleClient(t, helper.Org1.Admin) + + // Prepare two folders for label sync update scenario + common.CreateTestFolder(t, helper, "test-folder-a") + common.CreateTestFolder(t, helper, "test-folder-b") + + baseGen := ngmodels.RuleGen.With( + ngmodels.RuleMuts.WithUniqueUID(), + ngmodels.RuleMuts.WithUniqueTitle(), + ngmodels.RuleMuts.WithNamespaceUID("test-folder-a"), + ngmodels.RuleMuts.WithGroupName("test-group"), + ngmodels.RuleMuts.WithAllRecordingRules(), + ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second), + ) + + t.Run("should keep folder label in sync with folder annotation on create and update", func(t *testing.T) { + rule := baseGen.Generate() + recordingRule := &v0alpha1.RecordingRule{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + v0alpha1.FolderAnnotationKey: "test-folder-a", + }, + }, + Spec: v0alpha1.RecordingRuleSpec{ + Title: rule.Title, + Metric: rule.Record.Metric, + Expressions: v0alpha1.RecordingRuleExpressionMap{ + "A": { + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), + Model: rule.Data[0].Model, + Source: util.Pointer(true), + RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ + From: v0alpha1.RecordingRulePromDurationWMillis("5m"), + To: v0alpha1.RecordingRulePromDurationWMillis("0s"), + }, + }, + }, + Trigger: v0alpha1.RecordingRuleIntervalTrigger{Interval: v0alpha1.RecordingRulePromDuration("10s")}, + }, + } + + created, err := client.Create(ctx, recordingRule, v1.CreateOptions{}) + require.NoError(t, err) + defer func() { _ = client.Delete(ctx, created.Name, v1.DeleteOptions{}) }() + + // On create, metadata.labels[v0alpha1.FolderLabelKey] should mirror annotation + require.Equal(t, "test-folder-a", created.Labels[v0alpha1.FolderLabelKey]) + + updated := created.Copy().(*v0alpha1.RecordingRule) + if updated.Annotations == nil { + updated.Annotations = map[string]string{} + } + updated.Annotations[v0alpha1.FolderAnnotationKey] = "test-folder-b" + + after, err := client.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.Equal(t, "test-folder-b", after.Annotations[v0alpha1.FolderAnnotationKey]) + require.Equal(t, "test-folder-b", after.Labels[v0alpha1.FolderLabelKey]) + }) + + t.Run("should fail to create recording rule without folder annotation", func(t *testing.T) { + rule := baseGen.Generate() + recordingRule := &v0alpha1.RecordingRule{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{}, + }, + Spec: v0alpha1.RecordingRuleSpec{ + Title: rule.Title, + Metric: rule.Record.Metric, + Expressions: v0alpha1.RecordingRuleExpressionMap{ + "A": { + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), + Model: rule.Data[0].Model, + Source: util.Pointer(true), + RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ + From: v0alpha1.RecordingRulePromDurationWMillis("5m"), + To: v0alpha1.RecordingRulePromDurationWMillis("0s"), + }, + }, + }, + Trigger: v0alpha1.RecordingRuleIntervalTrigger{Interval: v0alpha1.RecordingRulePromDuration("10s")}, + }, + } + + created, err := client.Create(ctx, recordingRule, v1.CreateOptions{}) + require.Error(t, err) + require.Nil(t, created) + }) + + t.Run("should fail to create rule with group labels preset", func(t *testing.T) { + rule := baseGen.Generate() + recordingRule := &v0alpha1.RecordingRule{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + v0alpha1.FolderAnnotationKey: "test-folder-a", + }, + Labels: map[string]string{ + v0alpha1.GroupLabelKey: "some-group", + v0alpha1.GroupIndexLabelKey: "0", + }, + }, + Spec: v0alpha1.RecordingRuleSpec{ + Title: rule.Title, + Metric: rule.Record.Metric, + Expressions: v0alpha1.RecordingRuleExpressionMap{ + "A": { + QueryType: util.Pointer(rule.Data[0].QueryType), + DatasourceUID: util.Pointer(v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID)), + Model: rule.Data[0].Model, + Source: util.Pointer(true), + RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{ + From: v0alpha1.RecordingRulePromDurationWMillis("5m"), + To: v0alpha1.RecordingRulePromDurationWMillis("0s"), + }, + }, + }, + Trigger: v0alpha1.RecordingRuleIntervalTrigger{Interval: v0alpha1.RecordingRulePromDuration("10s")}, + }, + } + + created, err := client.Create(ctx, recordingRule, v1.CreateOptions{}) + require.Error(t, err) + require.Nil(t, created) + }) +} diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index e6739330286..09567f506aa 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -35,8 +35,18 @@ func TestMain(m *testing.M) { testsuite.Run(m) } -func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper, gvr schema.GroupVersionResource) { +func runDashboardTest(t *testing.T, mode rest.DualWriterMode, gvr schema.GroupVersionResource) { t.Run("simple crud+list", func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, + }, + }) + t.Cleanup(helper.Shutdown) + ctx := context.Background() client := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, @@ -128,15 +138,7 @@ func TestIntegrationDashboardsAppV0Alpha1(t *testing.T) { modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} for _, mode := range modes { t.Run(fmt.Sprintf("v0alpha1 with dual writer mode %d", mode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: mode, - }, - }, - }) - runDashboardTest(t, helper, gvr) + runDashboardTest(t, mode, gvr) }) } } @@ -152,15 +154,7 @@ func TestIntegrationDashboardsAppV1(t *testing.T) { modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} for _, mode := range modes { t.Run(fmt.Sprintf("v1beta1 with dual writer mode %d", mode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: mode, - }, - }, - }) - runDashboardTest(t, helper, gvr) + runDashboardTest(t, mode, gvr) }) } } @@ -176,15 +170,7 @@ func TestIntegrationDashboardsAppV2alpha1(t *testing.T) { modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} for _, mode := range modes { t.Run(fmt.Sprintf("v2alpha1 with dual writer mode %d", mode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: mode, - }, - }, - }) - runDashboardTest(t, helper, gvr) + runDashboardTest(t, mode, gvr) }) } } @@ -200,15 +186,7 @@ func TestIntegrationDashboardsAppV2beta1(t *testing.T) { modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} for _, mode := range modes { t.Run(fmt.Sprintf("v1alpha2 with dual writer mode %d", mode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: mode, - }, - }, - }) - runDashboardTest(t, helper, gvr) + runDashboardTest(t, mode, gvr) }) } } diff --git a/pkg/tests/apis/dashboard/testdata/panel-generate.json b/pkg/tests/apis/dashboard/testdata/panel-generate.json new file mode 100644 index 00000000000..93c7f1d2241 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/panel-generate.json @@ -0,0 +1,101 @@ +{ + "kind": "LibraryPanel", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "generateName": "x" + }, + "spec": { + "type": "timeseries", + "pluginVersion": "12.3.0-pre", + "title": "example library panel", + "panelTitle": "simple", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "cejobd88i85j4d" + }, + "gridPos": { + "w": 12, + "h": 8, + "x": 0, + "y": 0 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "cejobd88i85j4d" + } + } + ] + } +} \ No newline at end of file diff --git a/pkg/tests/apis/dashboard/testdata/panel-test.json b/pkg/tests/apis/dashboard/testdata/panel-test.json new file mode 100644 index 00000000000..d7e13d18653 --- /dev/null +++ b/pkg/tests/apis/dashboard/testdata/panel-test.json @@ -0,0 +1,101 @@ +{ + "kind": "LibraryPanel", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "test1" + }, + "spec": { + "type": "timeseries", + "pluginVersion": "12.3.0-pre", + "title": "example library panel (test1)", + "panelTitle": "simple", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "cejobd88i85j4d" + }, + "gridPos": { + "w": 12, + "h": 8, + "x": 0, + "y": 0 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "cejobd88i85j4d" + } + } + ] + } +} \ No newline at end of file diff --git a/pkg/tests/apis/datasource/testdata_test.go b/pkg/tests/apis/datasource/testdata_test.go index 5faf459bba9..9a94bea5dce 100644 --- a/pkg/tests/apis/datasource/testdata_test.go +++ b/pkg/tests/apis/datasource/testdata_test.go @@ -3,10 +3,12 @@ package dashboards import ( "context" "encoding/json" + "errors" "fmt" "testing" "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -101,19 +103,26 @@ func TestIntegrationTestDatasource(t *testing.T) { require.Len(t, list.Items, 1, "expected a single connection") require.Equal(t, "test", list.Items[0].GetName(), "with the test uid") - rsp, err := client.Get(ctx, "test", metav1.GetOptions{}, "health") - require.NoError(t, err) - body, err := rsp.MarshalJSON() - require.NoError(t, err) - //fmt.Printf("GOT: %v\n", string(body)) - require.JSONEq(t, `{ - "apiVersion": "testdata.datasource.grafana.app/v0alpha1", - "code": 1, - "kind": "HealthCheckResult", - "message": "Data source is working", - "status": "OK" - } - `, string(body)) + _, err = client.Get(ctx, "test", metav1.GetOptions{}, "health") + // endpoint is disabled currently because it has not been + // sufficiently tested. + // for more info see pkg/registry/apis/datasource/sub_health.go + require.Error(t, err) + var statusErr *apierrors.StatusError + require.True(t, errors.As(err, &statusErr)) + require.Equal(t, int32(501), statusErr.ErrStatus.Code) + // require.NoError(t, err) + // body, err := rsp.MarshalJSON() + // require.NoError(t, err) + // //fmt.Printf("GOT: %v\n", string(body)) + // require.JSONEq(t, `{ + // "apiVersion": "testdata.datasource.grafana.app/v0alpha1", + // "code": 1, + // "kind": "HealthCheckResult", + // "message": "Data source is working", + // "status": "OK" + // } + // `, string(body)) // Test connecting to non-JSON marshaled data raw := apis.DoRequest[any](helper, apis.RequestParams{ @@ -121,6 +130,10 @@ func TestIntegrationTestDatasource(t *testing.T) { Method: "GET", Path: "/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test/resource", }, nil) - require.Equal(t, `Hello world from test datasource!`, string(raw.Body)) + // endpoint is disabled currently because it has not been + // sufficiently tested. + // for more info see pkg/registry/apis/datasource/sub_resource.go + require.Equal(t, int32(501), raw.Status.Code) + // require.Equal(t, `Hello world from test datasource!`, string(raw.Body)) }) } diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 3449f50372c..4e272df1825 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -7,9 +7,11 @@ import ( "fmt" "net/http" "slices" + "strings" "testing" "time" + "github.com/google/uuid" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/meta" @@ -54,12 +56,11 @@ func TestIntegrationFoldersApp(t *testing.T) { t.Skip("test only on sqlite for now") } - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - EnableFeatureToggles: []string{}, - }) - t.Run("Check discovery client", func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + EnableFeatureToggles: []string{}, + }) disco := helper.NewDiscoveryClient() resources, err := disco.ServerResourcesForGroupVersion("folder.grafana.app/v1beta1") require.NoError(t, err) @@ -730,50 +731,74 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { // test on all dualwriter modes for mode := 0; mode <= 4; mode++ { - for _, tc := range tcs { - t.Run(fmt.Sprintf("[Mode: %v] "+tc.description, mode), func(t *testing.T) { - modeDw := grafanarest.DualWriterMode(mode) - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: modeDw, - }, + t.Run(fmt.Sprintf("Mode_%d", mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, }, - }) - - user := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) - - parentPayload := `{ - "title": "Test/parent", - "uid": "parentuid" - }` - parentCreate := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(parentPayload), - }, &folder.Folder{}) - require.NotNil(t, parentCreate.Result) - parentUID := parentCreate.Result.UID - require.NotEmpty(t, parentUID) - - resp := apis.DoRequest(helper, apis.RequestParams{ - User: user, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(tc.input), - }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, resp.Response.StatusCode) - - if tc.expectedCode == http.StatusOK { - require.Equal(t, "uid", resp.Result.UID) - require.Equal(t, "Folder", resp.Result.Title) - } + }, }) - } + for i, tc := range tcs { + t.Run(fmt.Sprintf("[Mode: %v] "+tc.description, mode), func(t *testing.T) { + username := fmt.Sprintf("user-%d", i) + parentUID := fmt.Sprintf("parentuid-%d", i) + childUID := fmt.Sprintf("uid-%d", i) + + // Update permissions to use unique parent UID + permissions := make([]resourcepermissions.SetResourcePermissionCommand, len(tc.permissions)) + for j, perm := range tc.permissions { + permissions[j] = perm + if perm.ResourceID == "parentuid" { + permissions[j].ResourceID = parentUID + } + } + + user := helper.CreateUser(username, apis.Org1, org.RoleViewer, permissions) + + // Get user ID for cleanup + userID, _ := user.Identity.GetInternalID() + + // Register cleanup for this test case + t.Cleanup(helper.CleanupTestResources([]string{parentUID, childUID}, []int64{userID})) + + parentPayload := fmt.Sprintf(`{ + "title": "Test/parent", + "uid": "%s" + }`, parentUID) + parentCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(parentPayload), + }, &folder.Folder{}) + require.NotNil(t, parentCreate.Result) + createdParentUID := parentCreate.Result.UID + require.NotEmpty(t, createdParentUID) + + // Update input to use unique UIDs + input := strings.ReplaceAll(tc.input, "parentuid", parentUID) + input = strings.ReplaceAll(input, `"uid": "uid"`, fmt.Sprintf(`"uid": "%s"`, childUID)) + + resp := apis.DoRequest(helper, apis.RequestParams{ + User: user, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(input), + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, resp.Response.StatusCode) + + if tc.expectedCode == http.StatusOK { + require.Equal(t, childUID, resp.Result.UID) + require.Equal(t, "Folder", resp.Result.Title) + } + }) + } + }) } } @@ -833,98 +858,137 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { // test on all dualwriter modes for mode := 0; mode <= 4; mode++ { - for _, tc := range tcs { - t.Run(fmt.Sprintf("[Mode: %v] "+tc.description, mode), func(t *testing.T) { - modeDw := grafanarest.DualWriterMode(mode) - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: modeDw, - }, + t.Run(fmt.Sprintf("Mode_%d", mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, }, - }) - - // Create parent folder - parentPayload := `{ - "title": "testparent", - "uid": "parentuid" - }` - parentCreate := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(parentPayload), - }, &folder.Folder{}) - require.NotNil(t, parentCreate.Result) - parentUID := parentCreate.Result.UID - require.NotEmpty(t, parentUID) - - // Create descendant folder - payload := "{ \"uid\": \"descuid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" - resp := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(payload), - }, &dtos.Folder{}) - require.Equal(t, http.StatusOK, resp.Response.StatusCode) - - user := helper.CreateUser("user", apis.Org1, org.RoleNone, tc.permissions) - - // Get with accesscontrol disabled - getResp := apis.DoRequest(helper, apis.RequestParams{ - User: user, - Method: http.MethodGet, - Path: "/api/folders/descuid", - }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, getResp.Response.StatusCode) - require.NotNil(t, getResp.Result) - - require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersRead]) - require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersWrite]) - - parents := getResp.Result.Parents - require.Equal(t, len(tc.expectedParentUIDs), len(parents)) - require.Equal(t, len(tc.expectedParentTitles), len(parents)) - for i := 0; i < len(tc.expectedParentUIDs); i++ { - require.Equal(t, tc.expectedParentUIDs[i], parents[i].UID) - require.Equal(t, tc.expectedParentTitles[i], parents[i].Title) - } - - // Get with accesscontrol enabled - if tc.checkAccessControl { - acPerms := []resourcepermissions.SetResourcePermissionCommand{ - { - Actions: []string{dashboards.ActionFoldersRead}, - Resource: "folders", - ResourceAttribute: "uid", - ResourceID: "*", - }, - { - Actions: []string{dashboards.ActionFoldersWrite}, - Resource: "folders", - ResourceAttribute: "uid", - ResourceID: "parentuid", - }, - } - acUser := helper.CreateUser("acuser", apis.Org1, org.RoleNone, acPerms) - - getWithAC := apis.DoRequest(helper, apis.RequestParams{ - User: acUser, - Method: http.MethodGet, - Path: "/api/folders/descuid?accesscontrol=true", - }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, getWithAC.Response.StatusCode) - require.NotNil(t, getWithAC.Result) - - require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersRead]) - require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersWrite]) - } + }, }) - } + + // Run all test cases within the same server instance + for i, tc := range tcs { + t.Run(tc.description, func(t *testing.T) { + // Use unique UIDs per test case to avoid conflicts + parentUID := fmt.Sprintf("parentuid-%d", i) + descUID := fmt.Sprintf("descuid-%d", i) + parentTitle := fmt.Sprintf("testparent-%d", i) + userLogin := fmt.Sprintf("user-%d", i) + acUserLogin := fmt.Sprintf("acuser-%d", i) + + // Create parent folder + parentPayload := fmt.Sprintf(`{ + "title": "%s", + "uid": "%s" + }`, parentTitle, parentUID) + parentCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(parentPayload), + }, &folder.Folder{}) + require.NotNil(t, parentCreate.Result) + require.Equal(t, parentUID, parentCreate.Result.UID) + + // Create descendant folder + payload := fmt.Sprintf(`{ "uid": "%s", "title": "Folder-%d", "parentUid": "%s"}`, descUID, i, parentUID) + resp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(payload), + }, &dtos.Folder{}) + require.Equal(t, http.StatusOK, resp.Response.StatusCode) + + // Update permissions to use unique descUID where needed + permissions := tc.permissions + for j := range permissions { + if permissions[j].ResourceID == "descuid" { + permissions[j].ResourceID = descUID + } + } + + user := helper.CreateUser(userLogin, apis.Org1, org.RoleNone, permissions) + + // Get user ID for cleanup + userID, err := user.Identity.GetInternalID() + require.NoError(t, err) + + // Register cleanup to delete created resources + t.Cleanup(helper.CleanupTestResources([]string{descUID, parentUID}, []int64{userID})) + + // Adjust expected UIDs and titles + expectedParentUIDs := tc.expectedParentUIDs + expectedParentTitles := tc.expectedParentTitles + if len(expectedParentUIDs) > 0 { + expectedParentUIDs = []string{parentUID} + expectedParentTitles = []string{parentTitle} + } + + // Get with accesscontrol disabled + getResp := apis.DoRequest(helper, apis.RequestParams{ + User: user, + Method: http.MethodGet, + Path: "/api/folders/" + descUID, + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, getResp.Response.StatusCode) + + if tc.expectedCode == http.StatusOK { + require.NotNil(t, getResp.Result) + require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersRead]) + require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersWrite]) + + parents := getResp.Result.Parents + require.Equal(t, len(expectedParentUIDs), len(parents)) + require.Equal(t, len(expectedParentTitles), len(parents)) + for j := 0; j < len(expectedParentUIDs); j++ { + require.Equal(t, expectedParentUIDs[j], parents[j].UID) + require.Equal(t, expectedParentTitles[j], parents[j].Title) + } + + // Get with accesscontrol enabled + if tc.checkAccessControl { + acPerms := []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{dashboards.ActionFoldersRead}, + Resource: "folders", + ResourceAttribute: "uid", + ResourceID: "*", + }, + { + Actions: []string{dashboards.ActionFoldersWrite}, + Resource: "folders", + ResourceAttribute: "uid", + ResourceID: parentUID, + }, + } + acUser := helper.CreateUser(acUserLogin, apis.Org1, org.RoleNone, acPerms) + + // Get user ID for cleanup + acUserID, err := acUser.Identity.GetInternalID() + require.NoError(t, err) + t.Cleanup(helper.CleanupTestResources([]string{}, []int64{acUserID})) + + getWithAC := apis.DoRequest(helper, apis.RequestParams{ + User: acUser, + Method: http.MethodGet, + Path: "/api/folders/" + descUID + "?accesscontrol=true", + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, getWithAC.Response.StatusCode) + require.NotNil(t, getWithAC.Result) + + require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersRead]) + require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersWrite]) + } + } + }) + } + }) } } @@ -1012,21 +1076,26 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { // test on all dualwriter modes for mode := 0; mode <= 4; mode++ { - for _, tc := range tcs { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + }) + for i, tc := range tcs { t.Run(fmt.Sprintf("[Mode: %v] "+testDescription(tc.description, tc.expectedFolderSvcError), mode), func(t *testing.T) { - modeDw := grafanarest.DualWriterMode(mode) - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: modeDw, - }, - }, - }) + username := fmt.Sprintf("user-%d", i) + folderUID := fmt.Sprintf("uid-%d", i) - userTest := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) + // Update input to use unique UIDs + input := strings.ReplaceAll(tc.input, `"uid": "uid"`, fmt.Sprintf(`"uid": "%s"`, folderUID)) + + userTest := helper.CreateUser(username, apis.Org1, org.RoleViewer, tc.permissions) if tc.createSecondRecord { client := helper.GetResourceClient(apis.ResourceClientArgs{ @@ -1037,7 +1106,7 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { User: client.Args.User, Method: http.MethodPost, Path: "/api/folders", - Body: []byte(tc.input), + Body: []byte(input), }, &folder.Folder{}) require.NotEmpty(t, create2.Response) require.Equal(t, http.StatusOK, create2.Response.StatusCode) @@ -1045,13 +1114,13 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { addr := helper.GetEnv().Server.HTTPServer.Listener.Addr() login := userTest.Identity.GetLogin() - baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password("user"), addr) + baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password(username), addr) req, err := http.NewRequest(http.MethodPost, fmt.Sprintf( "%s%s", baseUrl, "/api/folders", - ), bytes.NewBuffer([]byte(tc.input))) + ), bytes.NewBuffer([]byte(input))) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") @@ -1071,7 +1140,7 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { require.NoError(t, resp.Body.Close()) if tc.expectedCode == http.StatusOK { - require.Equal(t, "uid", folder.UID) + require.Equal(t, folderUID, folder.UID) require.Equal(t, "Folder", folder.Title) } @@ -1179,78 +1248,109 @@ func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) { }, } - // test on all dualwriter modes for mode := 0; mode <= 4; mode++ { - for _, tc := range tcs { - t.Run(fmt.Sprintf("Mode: %d, %s", mode, tc.description), func(t *testing.T) { - modeDw := grafanarest.DualWriterMode(mode) + t.Run(fmt.Sprintf("Mode_%d", mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: modeDw, - }, + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, }, - EnableFeatureToggles: []string{ - featuremgmt.FlagUnifiedStorageSearch, - }, - }) + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagUnifiedStorageSearch, + }, + }) - userTest := helper.CreateUser("user", apis.Org1, org.RoleNone, tc.permissions) + // Run all test cases within the same server instance + for i, tc := range tcs { + t.Run(tc.description, func(t *testing.T) { + // Use unique UIDs for folders to avoid conflicts between test cases + userTest := helper.CreateUser(fmt.Sprintf("user-%d", i), apis.Org1, org.RoleNone, tc.permissions) - for _, f := range tc.createFolders { - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: userTest, - GVR: gvr, - }) - create2 := apis.DoRequest(helper, apis.RequestParams{ - User: client.Args.User, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(f), - }, &folder.Folder{}) - require.NotEmpty(t, create2.Response) - require.Equal(t, http.StatusOK, create2.Response.StatusCode) - } + // Create folders with unique UIDs per test case + for _, f := range tc.createFolders { + // Replace hardcoded UIDs with unique ones + uniqueFolder := f + uniqueFolder = strings.Replace(uniqueFolder, `"foo"`, fmt.Sprintf(`"foo-%d"`, i), 1) + uniqueFolder = strings.Replace(uniqueFolder, `"bar"`, fmt.Sprintf(`"bar-%d"`, i), 1) + uniqueFolder = strings.Replace(uniqueFolder, `"qux"`, fmt.Sprintf(`"qux-%d"`, i), 1) + uniqueFolder = strings.Replace(uniqueFolder, `"parentUid": "foo"`, fmt.Sprintf(`"parentUid": "foo-%d"`, i), 1) - addr := helper.GetEnv().Server.HTTPServer.Listener.Addr() - login := userTest.Identity.GetLogin() - baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password("user"), addr) - - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf( - "%s%s", - baseUrl, - fmt.Sprintf("/api/folders%s", tc.params), - ), nil) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - if tc.requestToAnotherOrg { - req.Header.Set("x-grafana-org-id", "2") - } - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - require.NotNil(t, resp) - require.Equal(t, tc.expectedCode, resp.StatusCode) - - if tc.expectedCode == http.StatusOK { - list := []dtos.FolderSearchHit{} - err = json.NewDecoder(resp.Body).Decode(&list) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) - - // ignore IDs - for i := 0; i < len(list); i++ { - list[i].ID = 0 + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: userTest, + GVR: gvr, + }) + create2 := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(uniqueFolder), + }, &folder.Folder{}) + require.NotEmpty(t, create2.Response) + require.Equal(t, http.StatusOK, create2.Response.StatusCode) } - require.ElementsMatch(t, tc.expectedOutput, list) - } - }) - } + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr() + login := userTest.Identity.GetLogin() + baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password(fmt.Sprintf("user-%d", i)), addr) + + // Adjust params with unique UIDs + params := tc.params + params = strings.ReplaceAll(params, "foo", fmt.Sprintf("foo-%d", i)) + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf( + "%s%s", + baseUrl, + fmt.Sprintf("/api/folders%s", params), + ), nil) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + if tc.requestToAnotherOrg { + req.Header.Set("x-grafana-org-id", "2") + } + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, tc.expectedCode, resp.StatusCode) + + if tc.expectedCode == http.StatusOK { + list := []dtos.FolderSearchHit{} + err = json.NewDecoder(resp.Body).Decode(&list) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + // Adjust expected output with unique UIDs + expectedOutput := make([]dtos.FolderSearchHit, len(tc.expectedOutput)) + for j, output := range tc.expectedOutput { + expectedOutput[j] = output + expectedOutput[j].ID = 0 // ignore IDs + switch output.UID { + case "foo": + expectedOutput[j].UID = fmt.Sprintf("foo-%d", i) + case "bar": + expectedOutput[j].UID = fmt.Sprintf("bar-%d", i) + expectedOutput[j].ParentUID = fmt.Sprintf("foo-%d", i) + case "qux": + expectedOutput[j].UID = fmt.Sprintf("qux-%d", i) + } + } + + // ignore IDs in actual list + for j := 0; j < len(list); j++ { + list[j].ID = 0 + } + + require.ElementsMatch(t, expectedOutput, list) + } + }) + } + }) } } @@ -1262,8 +1362,7 @@ func TestIntegrationFolderDeletionBlockedByLibraryElements(t *testing.T) { t.Skip("test only on sqlite for now") } - // test on all dualwriter modes - for mode := 0; mode <= 2; mode++ { + for mode := 0; mode <= 5; mode++ { t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, delete blocked by library elements)", grafanarest.DualWriterMode(mode)), func(t *testing.T) { modeDw := grafanarest.DualWriterMode(mode) @@ -1341,7 +1440,9 @@ func TestIntegrationRootFolderDeletionBlockedByLibraryElementsInSubfolder(t *tes t.Skip("test only on sqlite for now") } - for mode := 0; mode <= 5; mode++ { + // TODO: re-enable on mode 4 and 5 when we migrate /api to /apis for library connections, and begin to + // use search to return the connections, rather than the connections table. + for mode := 0; mode <= 3; mode++ { t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, delete parent blocked by library elements in child)", grafanarest.DualWriterMode(mode)), func(t *testing.T) { modeDw := grafanarest.DualWriterMode(mode) @@ -1427,6 +1528,289 @@ func TestIntegrationRootFolderDeletionBlockedByLibraryElementsInSubfolder(t *tes } } +// Test folder deletion with connected (in-use) library panels - should be blocked +func TestIntegrationFolderDeletionBlockedByConnectedLibraryPanels(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } + + // TODO: re-enable on mode 4 and 5 when we migrate /api to /apis for library connections, and begin to + // use search to return the connections, rather than the connections table. + for mode := 0; mode <= 3; mode++ { + t.Run(fmt.Sprintf("mode %v - delete blocked by connected library panels in folder and subfolder", grafanarest.DualWriterMode(mode)), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + "dashboards.dashboard.grafana.app": { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagUnifiedStorageSearch, + }, + }) + + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + + // Create parent and child folders + uid := uuid.NewString()[:8] + parentUID := fmt.Sprintf("connected-parent-%d-%s", mode, uid) + childUID := fmt.Sprintf("connected-child-%d-%s", mode, uid) + createTestFolder(t, helper, client, parentUID, fmt.Sprintf("Parent Folder %d-%s", mode, uid), "") + createTestFolder(t, helper, client, childUID, fmt.Sprintf("Child Folder %d-%s", mode, uid), parentUID) + + // Create library panels in both folders + parentLibPanelName := fmt.Sprintf("Connected LP in parent %d-%s", mode, uid) + childLibPanelName := fmt.Sprintf("Connected LP in child %d-%s", mode, uid) + parentLibPanelUID := createTestLibraryPanel(t, helper, client, parentLibPanelName, parentUID) + childLibPanelUID := createTestLibraryPanel(t, helper, client, childLibPanelName, childUID) + + // Create dashboards using library panels (makes them connected) + parentDashUID := createDashboardWithLibraryPanel(t, helper, client, + fmt.Sprintf("Dashboard with LP in parent %d-%s", mode, uid), + parentLibPanelUID, "Connected LP in parent", parentUID) + childDashUID := createDashboardWithLibraryPanel(t, helper, client, + fmt.Sprintf("Dashboard with LP in child %d-%s", mode, uid), + childLibPanelUID, "Connected LP in child", childUID) + + // Attempt to delete the parent folder - should be blocked because library panels are connected + parentDelete := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodDelete, + Path: "/api/folders/" + parentUID, + }, &folder.Folder{}) + require.Equal(t, http.StatusForbidden, parentDelete.Response.StatusCode) + + // Verify both folders still exist + _, getParentErr := client.Resource.Get(context.Background(), parentUID, metav1.GetOptions{}) + require.NoError(t, getParentErr, "parent folder should still exist after failed deletion") + _, getChildErr := client.Resource.Get(context.Background(), childUID, metav1.GetOptions{}) + require.NoError(t, getChildErr, "child folder should still exist after failed deletion") + + // Verify library panels still exist + verifyLibraryPanelExists(t, helper, client, parentLibPanelUID) + verifyLibraryPanelExists(t, helper, client, childLibPanelUID) + + // Verify dashboards still exist + verifyDashboardExists(t, helper, client, parentDashUID) + verifyDashboardExists(t, helper, client, childDashUID) + }) + } +} + +// Test folder deletion with dangling (unconnected) library panels - should succeed and clean up +func TestIntegrationFolderDeletionWithDanglingLibraryPanels(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } + + for mode := 0; mode <= 5; mode++ { + t.Run(fmt.Sprintf("mode %v - delete succeeds and cleans up dangling library panels in folder and subfolder", grafanarest.DualWriterMode(mode)), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagUnifiedStorageSearch, + }, + }) + + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + + // Create parent and child folders + uid := uuid.NewString()[:8] + parentUID := fmt.Sprintf("dangling-parent-%d-%s", mode, uid) + childUID := fmt.Sprintf("dangling-child-%d-%s", mode, uid) + createTestFolder(t, helper, client, parentUID, fmt.Sprintf("Parent Folder %d-%s", mode, uid), "") + createTestFolder(t, helper, client, childUID, fmt.Sprintf("Child Folder %d-%s", mode, uid), parentUID) + + // Create dangling library panels in both folders (not connected to any dashboard) + parentLibPanelUID := createTestLibraryPanel(t, helper, client, + fmt.Sprintf("Dangling LP in parent %d-%s", mode, uid), parentUID) + childLibPanelUID := createTestLibraryPanel(t, helper, client, + fmt.Sprintf("Dangling LP in child %d-%s", mode, uid), childUID) + + // Verify library panels exist before deletion + verifyLibraryPanelExists(t, helper, client, parentLibPanelUID) + verifyLibraryPanelExists(t, helper, client, childLibPanelUID) + + // Attempt to delete the parent folder - should be blocked because library panels are connected + parentDelete := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodDelete, + Path: "/api/folders/" + parentUID, + }, &folder.Folder{}) + require.Equal(t, http.StatusOK, parentDelete.Response.StatusCode, parentDelete.Body) + // Verify folders are deleted + _, getParentErr := client.Resource.Get(context.Background(), parentUID, metav1.GetOptions{}) + require.Error(t, getParentErr, "parent folder should not exist after deletion") + _, getChildErr := client.Resource.Get(context.Background(), childUID, metav1.GetOptions{}) + require.Error(t, getChildErr, "child folder should not exist after deletion") + + // Verify dangling library panels were cleaned up + verifyLibraryPanelDeleted(t, helper, client, parentLibPanelUID, "dangling library panel in parent should be deleted") + verifyLibraryPanelDeleted(t, helper, client, childLibPanelUID, "dangling library panel in child should be deleted") + }) + } +} + +// Helper function to create a folder with specified UID and optional parent +func createTestFolder(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, uid, title, parentUID string) *folder.Folder { + t.Helper() + + payload := fmt.Sprintf(`{ + "title": "%s", + "uid": "%s"`, title, uid) + + if parentUID != "" { + payload += fmt.Sprintf(`, + "parentUid": "%s"`, parentUID) + } + + payload += "}" + + folderCreate := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(payload), + }, &folder.Folder{}) + + require.NotNil(t, folderCreate.Result) + require.Equal(t, uid, folderCreate.Result.UID) + + return folderCreate.Result +} + +// Helper function to create a library panel in a folder +func createTestLibraryPanel(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, name, folderUID string) string { + t.Helper() + + libPanelPayload := fmt.Sprintf(`{ + "kind": 1, + "name": "%s", + "folderUid": "%s", + "model": { + "type": "text", + "title": "%s" + } + }`, name, folderUID, name) + + libCreate := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/library-elements", + Body: []byte(libPanelPayload), + }, &map[string]interface{}{}) + + require.NotNil(t, libCreate.Response) + require.Equal(t, http.StatusOK, libCreate.Response.StatusCode) + + libPanelUID := (*libCreate.Result)["result"].(map[string]interface{})["uid"].(string) + require.NotEmpty(t, libPanelUID) + + return libPanelUID +} + +// Helper function to create a dashboard that uses a library panel +func createDashboardWithLibraryPanel(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, dashTitle, libPanelUID, libPanelName, folderUID string) string { + t.Helper() + + dashPayload := fmt.Sprintf(`{ + "dashboard": { + "title": "%s", + "panels": [{ + "id": 1, + "libraryPanel": { + "uid": "%s", + "name": "%s" + } + }] + }, + "folderUid": "%s", + "overwrite": false + }`, dashTitle, libPanelUID, libPanelName, folderUID) + + dashCreate := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/dashboards/db", + Body: []byte(dashPayload), + }, &map[string]interface{}{}) + + require.NotNil(t, dashCreate.Response) + require.Equal(t, http.StatusOK, dashCreate.Response.StatusCode) + + // Extract dashboard UID from response + dashUID := (*dashCreate.Result)["uid"].(string) + require.NotEmpty(t, dashUID) + + return dashUID +} + +// Helper function to verify library panel exists +func verifyLibraryPanelExists(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, libPanelUID string) { + t.Helper() + + libGet := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodGet, + Path: fmt.Sprintf("/api/library-elements/%s", libPanelUID), + }, &map[string]interface{}{}) + + require.Equal(t, http.StatusOK, libGet.Response.StatusCode) +} + +// Helper function to verify library panel does not exist +func verifyLibraryPanelDeleted(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, libPanelUID, message string) { + t.Helper() + + libGet := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodGet, + Path: fmt.Sprintf("/api/library-elements/%s", libPanelUID), + }, &map[string]interface{}{}) + + require.Equal(t, http.StatusNotFound, libGet.Response.StatusCode, message) +} + +// Helper function to verify dashboard exists by UID +func verifyDashboardExists(t *testing.T, helper *apis.K8sTestHelper, client *apis.K8sResourceClient, dashUID string) { + t.Helper() + + dashGet := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodGet, + Path: fmt.Sprintf("/api/dashboards/uid/%s", dashUID), + }, &map[string]interface{}{}) + + require.Equal(t, http.StatusOK, dashGet.Response.StatusCode, fmt.Sprintf("dashboard %s should still exist", dashUID)) +} + // Test moving folders to root. func TestIntegrationMoveNestedFolderToRootK8S(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -1501,3 +1885,106 @@ func TestIntegrationMoveNestedFolderToRootK8S(t *testing.T) { require.Equal(t, "f2", get.Result.UID) require.Equal(t, "", get.Result.ParentUID) } + +// Test deleting nested folders ensures postorder deletion +func TestIntegrationDeleteNestedFoldersPostorder(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } + + for mode := 0; mode <= 5; mode++ { + t.Run(fmt.Sprintf("Mode %d: Delete nested folder hierarchy in postorder", mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagUnifiedStorageSearch, + }, + }) + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + // Helper function to create a folder and return its UID and ParentUID + createFolder := func(title, uid, parentUid string) (string, string) { + payload := fmt.Sprintf(`{"title":"%s","uid":"%s"%s}`, title, uid, func() string { + if parentUid != "" { + return fmt.Sprintf(`,"parentUid":"%s"`, parentUid) + } + return "" + }()) + create := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(payload), + }, &folder.Folder{}) + require.NotNil(t, create.Result) + require.Equal(t, http.StatusOK, create.Response.StatusCode) + return create.Result.UID, create.Result.ParentUID + } + + // Create a nested folder structure: + // parent + // / \ + // child1 child2 + // | + // grandchild + + // Create parent folder + parentUID, _ := createFolder(fmt.Sprintf("Parent-%d", mode), fmt.Sprintf("parent-%d", mode), "") + + // Create child1 folder + child1UID, child1ParentUID := createFolder(fmt.Sprintf("Child1-%d", mode), fmt.Sprintf("child1-%d", mode), parentUID) + require.Equal(t, parentUID, child1ParentUID) + + // Create child2 folder + child2UID, child2ParentUID := createFolder(fmt.Sprintf("Child2-%d", mode), fmt.Sprintf("child2-%d", mode), parentUID) + require.Equal(t, parentUID, child2ParentUID) + + // Create grandchild folder under child1 + grandchildUID, grandchildParentUID := createFolder(fmt.Sprintf("Grandchild-%d", mode), fmt.Sprintf("grandchild-%d", mode), child1UID) + require.Equal(t, child1UID, grandchildParentUID) + + // Verify the structure before deletion + verifyFolderExists := func(uid string, shouldExist bool) { + _, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + if shouldExist { + require.NoError(t, err, "folder %s should exist", uid) + } else { + require.Error(t, err, "folder %s should not exist", uid) + } + } + + // All folders should exist + verifyFolderExists(parentUID, true) + verifyFolderExists(child1UID, true) + verifyFolderExists(child2UID, true) + verifyFolderExists(grandchildUID, true) + + // Delete the parent folder - this should trigger postorder deletion + parentDelete := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodDelete, + Path: "/api/folders/" + parentUID, + }, &folder.Folder{}) + require.NotNil(t, parentDelete.Result) + require.Equal(t, http.StatusOK, parentDelete.Response.StatusCode) + + // All folders should now be deleted (postorder deletion: grandchild, child1, child2, parent) + verifyFolderExists(grandchildUID, false) + verifyFolderExists(child1UID, false) + verifyFolderExists(child2UID, false) + verifyFolderExists(parentUID, false) + }) + } +} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 4eca0e024b3..59b7d2c4c28 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -58,6 +58,24 @@ const ( Org2 = "OrgB" ) +var ( + sharedHTTPClient = &http.Client{ + Timeout: 30 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Transport: &http.Transport{ + MaxIdleConns: 1000, + MaxIdleConnsPerHost: 500, + MaxConnsPerHost: 500, + IdleConnTimeout: 90 * time.Second, + DisableKeepAlives: false, + DisableCompression: true, + ForceAttemptHTTP2: false, + }, + } +) + type K8sTestHelper struct { t *testing.T listenerAddress string @@ -498,12 +516,8 @@ func DoRequest[T any](c *K8sTestHelper, params RequestParams, result *T) K8sResp if params.Accept != "" { req.Header.Set("Accept", params.Accept) } - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } - rsp, err := client.Do(req) + + rsp, err := sharedHTTPClient.Do(req) require.NoError(c.t, err) r := K8sResponse[T]{ @@ -943,6 +957,50 @@ func (c *K8sTestHelper) DeleteServiceAccount(user User, orgID int64, saID int64) require.Equal(c.t, http.StatusOK, resp.Response.StatusCode, "failed to delete service account, body: %s", string(resp.Body)) } +func (c *K8sTestHelper) DeleteFolder(user User, folderUID string) error { + c.t.Helper() + + resp := DoRequest(c, RequestParams{ + User: user, + Method: http.MethodDelete, + Path: fmt.Sprintf("/api/folders/%s", folderUID), + }, &struct{}{}) + + if resp.Response.StatusCode != http.StatusOK && resp.Response.StatusCode != http.StatusNotFound { + return fmt.Errorf("failed to delete folder %s: status %d, body: %s", folderUID, resp.Response.StatusCode, string(resp.Body)) + } + return nil +} + +func (c *K8sTestHelper) DeleteUser(adminUser User, userID int64) error { + c.t.Helper() + + resp := DoRequest(c, RequestParams{ + User: adminUser, + Method: http.MethodDelete, + Path: fmt.Sprintf("/api/admin/users/%d", userID), + }, &struct{}{}) + + if resp.Response.StatusCode != http.StatusOK && resp.Response.StatusCode != http.StatusNotFound { + return fmt.Errorf("failed to delete user %d: status %d, body: %s", userID, resp.Response.StatusCode, string(resp.Body)) + } + return nil +} + +func (c *K8sTestHelper) CleanupTestResources(folderUIDs []string, userIDs []int64) func() { + return func() { + c.t.Helper() + // Delete folders first (they may have dependencies) + for _, uid := range folderUIDs { + _ = c.DeleteFolder(c.Org1.Admin, uid) + } + // Then delete users + for _, id := range userIDs { + _ = c.DeleteUser(c.Org1.Admin, id) + } + } +} + // Ensures that the passed error is an APIStatus error and fails the test if it is not. func (c *K8sTestHelper) RequireApiErrorStatus(err error, reason metav1.StatusReason, httpCode int) metav1.Status { require.Error(c.t, err) diff --git a/pkg/tests/apis/iam/team_integration_test.go b/pkg/tests/apis/iam/team_integration_test.go index e8bb115dfb7..ef804987fd4 100644 --- a/pkg/tests/apis/iam/team_integration_test.go +++ b/pkg/tests/apis/iam/team_integration_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" @@ -112,7 +113,7 @@ func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, "Failure", statusErr.ErrStatus.Status) - require.Contains(t, statusErr.ErrStatus.Message, "team not found") + require.Contains(t, statusErr.ErrStatus.Message, "not found") }) t.Run("should not be able to create team when using a user with insufficient permissions", func(t *testing.T) { @@ -222,6 +223,36 @@ func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { require.Equal(t, createdUID, fetched.GetName()) require.Equal(t, "default", fetched.GetNamespace()) + + // Cleanup + err = teamClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + + t.Run("should list teams correctly", func(t *testing.T) { + ctx := context.Background() + + teamClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrTeams, + }) + + // For ensuring that it is able to list a team which has external_uid = null and is_provisioned = null + // only matters when legacy storage is involved + env := helper.GetEnv() + res, err := env.SQLStore.GetSqlxSession().Exec(ctx, "INSERT INTO team (org_id, uid, name, email, is_provisioned, external_uid, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + helper.Org1.Admin.Identity.GetOrgID(), "t000000001", "List Team 1", "list-team-1@example.com", nil, nil, time.Now(), time.Now()) + require.NoError(t, err) + require.NotNil(t, res) + + list, err := teamClient.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.NotNil(t, list) + + // Cleanup + _, err = env.SQLStore.GetSqlxSession().Exec(ctx, "DELETE FROM team WHERE uid = ?", "t000000001") + require.NoError(t, err) }) } @@ -301,6 +332,6 @@ func doTeamCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper, var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, "Failure", statusErr.ErrStatus.Status) - require.Contains(t, statusErr.ErrStatus.Message, "team not found") + require.Contains(t, statusErr.ErrStatus.Message, "not found") }) } diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index e68f94dda3e..42af6d54a98 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -1812,6 +1812,14 @@ } } }, + { + "name": "libraryPanel", + "in": "query", + "description": "find dashboards that reference a given libraryPanel", + "schema": { + "type": "string" + } + }, { "name": "sort", "in": "query", @@ -2324,6 +2332,7 @@ "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.DashboardAccess": { "type": "object", "required": [ + "isPublic", "canSave", "canEdit", "canAdmin", @@ -2356,6 +2365,10 @@ "type": "boolean", "default": false }, + "isPublic": { + "type": "boolean", + "default": false + }, "slug": { "description": "Metadata fields", "type": "string" @@ -2694,7 +2707,7 @@ "items": { "$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery" }, - "x-kubernetes-list-type": "set" + "x-kubernetes-list-type": "atomic" }, "title": { "description": "The title of the library panel", diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v1beta1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v1beta1.json index 4ae094ff867..363af97fb34 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v1beta1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v1beta1.json @@ -1018,6 +1018,7 @@ "description": "Information about how the requesting user can use a given dashboard", "type": "object", "required": [ + "isPublic", "canSave", "canEdit", "canAdmin", @@ -1050,6 +1051,10 @@ "type": "boolean", "default": false }, + "isPublic": { + "type": "boolean", + "default": false + }, "slug": { "description": "Metadata fields", "type": "string" diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index d8962a72a5d..90e70f63baf 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -1019,6 +1019,7 @@ "description": "Information about how the requesting user can use a given dashboard", "type": "object", "required": [ + "isPublic", "canSave", "canEdit", "canAdmin", @@ -1051,6 +1052,10 @@ "type": "boolean", "default": false }, + "isPublic": { + "type": "boolean", + "default": false + }, "slug": { "description": "Metadata fields", "type": "string" @@ -3757,9 +3762,9 @@ "default": "" }, "value": { + "description": "Value null means -Infinity", "type": "number", - "format": "double", - "default": 0 + "format": "double" } } }, @@ -3918,6 +3923,10 @@ "properties" ], "properties": { + "__systemRef": { + "description": "Describes config override rules created when interacting with Grafana.", + "type": "string" + }, "matcher": { "default": {}, "allOf": [ diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index c247c727237..d0ac7bbfff3 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -82,6 +82,836 @@ } } }, + "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/externalgroupmappings": { + "get": { + "tags": [ + "ExternalGroupMapping" + ], + "description": "list or watch objects of kind ExternalGroupMapping", + "operationId": "listExternalGroupMapping", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "iam.grafana.app", + "version": "v0alpha1", + "kind": "ExternalGroupMapping" + } + }, + "post": { + "tags": [ + "ExternalGroupMapping" + ], + "description": "create an ExternalGroupMapping", + "operationId": "createExternalGroupMapping", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "iam.grafana.app", + "version": "v0alpha1", + "kind": "ExternalGroupMapping" + } + }, + "delete": { + "tags": [ + "ExternalGroupMapping" + ], + "description": "delete collection of ExternalGroupMapping", + "operationId": "deletecollectionExternalGroupMapping", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "iam.grafana.app", + "version": "v0alpha1", + "kind": "ExternalGroupMapping" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/externalgroupmappings/{name}": { + "get": { + "tags": [ + "ExternalGroupMapping" + ], + "description": "read the specified ExternalGroupMapping", + "operationId": "getExternalGroupMapping", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "iam.grafana.app", + "version": "v0alpha1", + "kind": "ExternalGroupMapping" + } + }, + "put": { + "tags": [ + "ExternalGroupMapping" + ], + "description": "replace the specified ExternalGroupMapping", + "operationId": "replaceExternalGroupMapping", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "iam.grafana.app", + "version": "v0alpha1", + "kind": "ExternalGroupMapping" + } + }, + "delete": { + "tags": [ + "ExternalGroupMapping" + ], + "description": "delete an ExternalGroupMapping", + "operationId": "deleteExternalGroupMapping", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "iam.grafana.app", + "version": "v0alpha1", + "kind": "ExternalGroupMapping" + } + }, + "patch": { + "tags": [ + "ExternalGroupMapping" + ], + "description": "partially update the specified ExternalGroupMapping", + "operationId": "updateExternalGroupMapping", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "iam.grafana.app", + "version": "v0alpha1", + "kind": "ExternalGroupMapping" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the ExternalGroupMapping", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/serviceaccounts": { "get": { "tags": [ @@ -4102,6 +4932,124 @@ }, "components": { "schemas": { + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping": { + "type": "object", + "required": [ + "metadata", + "spec" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the ExternalGroupMapping", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingSpec" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "ExternalGroupMapping", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMapping" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "ExternalGroupMappingList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingSpec": { + "type": "object", + "required": [ + "teamRef", + "externalGroupId" + ], + "properties": { + "externalGroupId": { + "type": "string", + "default": "" + }, + "teamRef": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingTeamRef" + } + ] + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ExternalGroupMappingTeamRef": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name is the unique identifier for a team.", + "type": "string", + "default": "" + } + } + }, "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccount": { "type": "object", "required": [ @@ -5210,6 +6158,85 @@ } } }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMapping": { + "type": "object", + "required": [ + "metadata", + "spec" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the ExternalGroupMapping", + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingSpec": { + "type": "object", + "required": [ + "teamRef", + "externalGroupId" + ], + "properties": { + "externalGroupId": { + "type": "string", + "default": "" + }, + "teamRef": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name is the unique identifier for a team.", + "type": "string", + "default": "" + } + } + }, "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": { "type": "object", "required": [ @@ -7053,6 +8080,32 @@ "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", "type": "string", "format": "date-time" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { + "description": "Event represents a single event to a watched resource.", + "type": "object", + "required": [ + "type", + "object" + ], + "properties": { + "object": { + "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" + } + ] + }, + "type": { + "type": "string", + "default": "" + } + } + }, + "io.k8s.apimachinery.pkg.runtime.RawExtension": { + "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", + "type": "object" } } } diff --git a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json index 7973627042e..2061616fc9a 100644 --- a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json @@ -865,296 +865,6 @@ } ] }, - "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/receivers/{name}/status": { - "get": { - "tags": [ - "Receiver" - ], - "description": "read status of the specified Receiver", - "operationId": "getReceiverStatus", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - } - } - } - }, - "x-kubernetes-action": "get", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "Receiver" - } - }, - "put": { - "tags": [ - "Receiver" - ], - "description": "replace status of the specified Receiver", - "operationId": "replaceReceiverStatus", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - } - } - } - }, - "x-kubernetes-action": "put", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "Receiver" - } - }, - "patch": { - "tags": [ - "Receiver" - ], - "description": "partially update status of the specified Receiver", - "operationId": "updateReceiverStatus", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "force", - "in": "query", - "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/apply-patch+yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/strategic-merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" - } - } - } - } - }, - "x-kubernetes-action": "patch", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "Receiver" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the Receiver", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] - }, "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/routingtrees": { "get": { "tags": [ @@ -1985,296 +1695,6 @@ } ] }, - "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/routingtrees/{name}/status": { - "get": { - "tags": [ - "RoutingTree" - ], - "description": "read status of the specified RoutingTree", - "operationId": "getRoutingTreeStatus", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - } - } - } - }, - "x-kubernetes-action": "get", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "RoutingTree" - } - }, - "put": { - "tags": [ - "RoutingTree" - ], - "description": "replace status of the specified RoutingTree", - "operationId": "replaceRoutingTreeStatus", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - } - } - } - }, - "x-kubernetes-action": "put", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "RoutingTree" - } - }, - "patch": { - "tags": [ - "RoutingTree" - ], - "description": "partially update status of the specified RoutingTree", - "operationId": "updateRoutingTreeStatus", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "force", - "in": "query", - "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/apply-patch+yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/strategic-merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" - } - } - } - } - }, - "x-kubernetes-action": "patch", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "RoutingTree" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the RoutingTree", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] - }, "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/templategroups": { "get": { "tags": [ @@ -3105,296 +2525,6 @@ } ] }, - "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/templategroups/{name}/status": { - "get": { - "tags": [ - "TemplateGroup" - ], - "description": "read status of the specified TemplateGroup", - "operationId": "getTemplateGroupStatus", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - } - } - } - }, - "x-kubernetes-action": "get", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "TemplateGroup" - } - }, - "put": { - "tags": [ - "TemplateGroup" - ], - "description": "replace status of the specified TemplateGroup", - "operationId": "replaceTemplateGroupStatus", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - } - } - } - }, - "x-kubernetes-action": "put", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "TemplateGroup" - } - }, - "patch": { - "tags": [ - "TemplateGroup" - ], - "description": "partially update status of the specified TemplateGroup", - "operationId": "updateTemplateGroupStatus", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "force", - "in": "query", - "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/apply-patch+yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/strategic-merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" - } - } - } - } - }, - "x-kubernetes-action": "patch", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "TemplateGroup" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the TemplateGroup", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] - }, "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/timeintervals": { "get": { "tags": [ @@ -4224,296 +3354,6 @@ } } ] - }, - "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/timeintervals/{name}/status": { - "get": { - "tags": [ - "TimeInterval" - ], - "description": "read status of the specified TimeInterval", - "operationId": "getTimeIntervalStatus", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - } - } - } - }, - "x-kubernetes-action": "get", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "TimeInterval" - } - }, - "put": { - "tags": [ - "TimeInterval" - ], - "description": "replace status of the specified TimeInterval", - "operationId": "replaceTimeIntervalStatus", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - } - } - } - }, - "x-kubernetes-action": "put", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "TimeInterval" - } - }, - "patch": { - "tags": [ - "TimeInterval" - ], - "description": "partially update status of the specified TimeInterval", - "operationId": "updateTimeIntervalStatus", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "force", - "in": "query", - "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/apply-patch+yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/strategic-merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" - } - } - } - } - }, - "x-kubernetes-action": "patch", - "x-kubernetes-group-version-kind": { - "group": "notifications.alerting.grafana.app", - "version": "v0alpha1", - "kind": "TimeInterval" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the TimeInterval", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] } }, "components": { @@ -4545,9 +3385,6 @@ }, "spec": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverSpec" - }, - "status": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverStatus" } }, "x-kubernetes-group-version-kind": [ @@ -4637,41 +3474,6 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string" - }, - "state": { - "description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.", - "type": "string", - "enum": [ - "success", - "in_progress", - "failed" - ] - } - }, - "additionalProperties": false - }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverSpec": { "type": "object", "required": [ @@ -4691,27 +3493,6 @@ }, "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverOperatorState" - } - } - }, - "additionalProperties": false - }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree": { "type": "object", "required": [ @@ -4739,9 +3520,6 @@ }, "spec": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeSpec" - }, - "status": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeStatus" } }, "x-kubernetes-group-version-kind": [ @@ -4821,41 +3599,6 @@ }, "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string" - }, - "state": { - "description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.", - "type": "string", - "enum": [ - "success", - "in_progress", - "failed" - ] - } - }, - "additionalProperties": false - }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeRoute": { "type": "object", "required": [ @@ -4956,27 +3699,6 @@ }, "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeOperatorState" - } - } - }, - "additionalProperties": false - }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup": { "type": "object", "required": [ @@ -5004,9 +3726,6 @@ }, "spec": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupSpec" - }, - "status": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupStatus" } }, "x-kubernetes-group-version-kind": [ @@ -5060,41 +3779,6 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string" - }, - "state": { - "description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.", - "type": "string", - "enum": [ - "success", - "in_progress", - "failed" - ] - } - }, - "additionalProperties": false - }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupSpec": { "type": "object", "required": [ @@ -5111,27 +3795,6 @@ }, "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupOperatorState" - } - } - }, - "additionalProperties": false - }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval": { "type": "object", "required": [ @@ -5159,9 +3822,6 @@ }, "spec": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalSpec" - }, - "status": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalStatus" } }, "x-kubernetes-group-version-kind": [ @@ -5254,41 +3914,6 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string" - }, - "state": { - "description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.", - "type": "string", - "enum": [ - "success", - "in_progress", - "failed" - ] - } - }, - "additionalProperties": false - }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalSpec": { "type": "object", "required": [ @@ -5308,27 +3933,6 @@ }, "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalOperatorState" - } - } - }, - "additionalProperties": false - }, "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalTimeRange": { "type": "object", "required": [ diff --git a/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1beta1.json similarity index 95% rename from pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json rename to pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1beta1.json index 574ce2f474a..88cabe3ce2d 100644 --- a/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/shorturl.grafana.app-v1beta1.json @@ -1,10 +1,10 @@ { "openapi": "3.0.0", "info": { - "title": "shorturl.grafana.app/v1alpha1" + "title": "shorturl.grafana.app/v1beta1" }, "paths": { - "/apis/shorturl.grafana.app/v1alpha1/": { + "/apis/shorturl.grafana.app/v1beta1/": { "get": { "tags": [ "API Discovery" @@ -35,7 +35,7 @@ } } }, - "/apis/shorturl.grafana.app/v1alpha1/namespaces/{namespace}/shorturls": { + "/apis/shorturl.grafana.app/v1beta1/namespaces/{namespace}/shorturls": { "get": { "tags": [ "ShortURL" @@ -140,27 +140,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLList" } } } @@ -169,7 +169,7 @@ "x-kubernetes-action": "list", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -212,17 +212,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } }, @@ -234,17 +234,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -254,17 +254,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -274,17 +274,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -293,7 +293,7 @@ "x-kubernetes-action": "post", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -447,7 +447,7 @@ "x-kubernetes-action": "deletecollection", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -473,7 +473,7 @@ } ] }, - "/apis/shorturl.grafana.app/v1alpha1/namespaces/{namespace}/shorturls/{name}": { + "/apis/shorturl.grafana.app/v1beta1/namespaces/{namespace}/shorturls/{name}": { "get": { "tags": [ "ShortURL" @@ -486,17 +486,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -505,7 +505,7 @@ "x-kubernetes-action": "get", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -548,17 +548,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } }, @@ -570,17 +570,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -590,17 +590,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -609,7 +609,7 @@ "x-kubernetes-action": "put", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -711,7 +711,7 @@ "x-kubernetes-action": "delete", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -790,17 +790,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -810,17 +810,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -829,7 +829,7 @@ "x-kubernetes-action": "patch", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -865,7 +865,7 @@ } ] }, - "/apis/shorturl.grafana.app/v1alpha1/namespaces/{namespace}/shorturls/{name}/goto": { + "/apis/shorturl.grafana.app/v1beta1/namespaces/{namespace}/shorturls/{name}/goto": { "get": { "tags": [ "ShortURL" @@ -878,7 +878,7 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.GetGoto" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.GetGoto" } } } @@ -887,7 +887,7 @@ "x-kubernetes-action": "connect", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ResourceCallOptions" } }, @@ -914,7 +914,7 @@ } ] }, - "/apis/shorturl.grafana.app/v1alpha1/namespaces/{namespace}/shorturls/{name}/status": { + "/apis/shorturl.grafana.app/v1beta1/namespaces/{namespace}/shorturls/{name}/status": { "get": { "tags": [ "ShortURL" @@ -927,17 +927,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -946,7 +946,7 @@ "x-kubernetes-action": "get", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -989,17 +989,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } }, @@ -1011,17 +1011,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -1031,17 +1031,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -1050,7 +1050,7 @@ "x-kubernetes-action": "put", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -1129,17 +1129,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -1149,17 +1149,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } } } @@ -1168,7 +1168,7 @@ "x-kubernetes-action": "patch", "x-kubernetes-group-version-kind": { "group": "shorturl.grafana.app", - "version": "v1alpha1", + "version": "v1beta1", "kind": "ShortURL" } }, @@ -1207,7 +1207,7 @@ }, "components": { "schemas": { - "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.GetGoto": { + "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.GetGoto": { "type": "object", "required": [ "url" @@ -1218,7 +1218,7 @@ } } }, - "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL": { + "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL": { "type": "object", "required": [ "kind", @@ -1244,21 +1244,21 @@ ] }, "spec": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLSpec" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLSpec" }, "status": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLStatus" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLStatus" } }, "x-kubernetes-group-version-kind": [ { "group": "shorturl.grafana.app", "kind": "ShortURL", - "version": "v1alpha1" + "version": "v1beta1" } ] }, - "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLList": { + "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLList": { "type": "object", "required": [ "metadata", @@ -1275,7 +1275,7 @@ "default": {}, "allOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURL" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURL" } ] } @@ -1297,11 +1297,11 @@ { "group": "shorturl.grafana.app", "kind": "ShortURLList", - "version": "v1alpha1" + "version": "v1beta1" } ] }, - "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLOperatorState": { + "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLOperatorState": { "type": "object", "required": [ "lastEvaluation", @@ -1336,7 +1336,7 @@ }, "additionalProperties": false }, - "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLSpec": { + "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLSpec": { "type": "object", "required": [ "path" @@ -1349,7 +1349,7 @@ }, "additionalProperties": false }, - "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLStatus": { + "com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLStatus": { "type": "object", "required": [ "lastSeenAt" @@ -1371,7 +1371,7 @@ "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1alpha1.ShortURLOperatorState" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.shorturl.pkg.apis.shorturl.v1beta1.ShortURLOperatorState" } } }, diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index 1e56532fb84..b218095d938 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -108,7 +108,7 @@ func TestIntegrationOpenAPIs(t *testing.T) { Version: "v0alpha1", }, { Group: "shorturl.grafana.app", - Version: "v1alpha1", + Version: "v1beta1", }, { Group: "testdata.datasource.grafana.app", Version: "v0alpha1", diff --git a/pkg/tests/apis/plugins/discovery_test.go b/pkg/tests/apis/plugins/discovery_test.go index 545f2f3bf7d..168aa259dad 100644 --- a/pkg/tests/apis/plugins/discovery_test.go +++ b/pkg/tests/apis/plugins/discovery_test.go @@ -20,19 +20,30 @@ func TestIntegrationPluginsIntegrationDiscovery(t *testing.T) { "freshness": "Current", "resources": [ { - "resource": "plugininstalls", + "resource": "plugins", "responseKind": { "group": "", - "kind": "PluginInstall", + "kind": "Plugin", "version": "" }, "scope": "Namespaced", - "singularResource": "plugininstalls", + "singularResource": "plugins", "subresources": [ { "responseKind": { "group": "", - "kind": "PluginInstall", + "kind": "ResourceCallOptions", + "version": "" + }, + "subresource": "meta", + "verbs": [ + "get" + ] + }, + { + "responseKind": { + "group": "", + "kind": "Plugin", "version": "" }, "subresource": "status", @@ -53,35 +64,6 @@ func TestIntegrationPluginsIntegrationDiscovery(t *testing.T) { "update", "watch" ] - }, - { - "resource": "pluginmetas", - "responseKind": { - "group": "", - "kind": "PluginMeta", - "version": "" - }, - "scope": "Namespaced", - "singularResource": "pluginmeta", - "subresources": [ - { - "responseKind": { - "group": "", - "kind": "PluginMeta", - "version": "" - }, - "subresource": "status", - "verbs": [ - "get", - "patch", - "update" - ] - } - ], - "verbs": [ - "get", - "list" - ] } ] } diff --git a/pkg/tests/apis/plugins/plugininstalls_test.go b/pkg/tests/apis/plugins/plugininstalls_test.go index a9ceecfd7aa..d8c45713337 100644 --- a/pkg/tests/apis/plugins/plugininstalls_test.go +++ b/pkg/tests/apis/plugins/plugininstalls_test.go @@ -16,88 +16,54 @@ import ( "github.com/grafana/grafana/pkg/util/testutil" ) -var gvrPluginInstalls = schema.GroupVersionResource{ +var gvrPlugins = schema.GroupVersionResource{ Group: "plugins.grafana.app", Version: "v0alpha1", - Resource: "plugininstalls", + Resource: "plugins", } func TestMain(m *testing.M) { testsuite.Run(m) } -func TestIntegrationPluginInstalls(t *testing.T) { +func TestIntegrationPlugins(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) - t.Run("create plugin install", func(t *testing.T) { + t.Run("create plugin", func(t *testing.T) { helper := setupHelper(t) ctx := context.Background() client := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, - GVR: gvrPluginInstalls, + GVR: gvrPlugins, }) pluginName := "test-plugin-create" - pluginInstall := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", + "kind": "Plugin", "metadata": {"name": "%s"}, "spec": {"version": "1.0.0"} }`, pluginName)) - created, err := client.Resource.Create(ctx, pluginInstall, metav1.CreateOptions{}) + created, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) require.Equal(t, pluginName, created.GetName()) }) - t.Run("create plugin install with status is ignored", func(t *testing.T) { - t.Skip("status is not ignored on create. this might require a change in the SDK. skipping for now") - helper := setupHelper(t) - ctx := context.Background() - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrPluginInstalls, - }) - pluginName := "test-plugin-create-with-status" - pluginInstall := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", - "metadata": {"name": "%s"}, - "spec": {"version": "1.0.0"}, - "status": { - "operatorStates": { - "test-operator": { - "lastEvaluation": "1", - "state": "success" - } - } - } - }`, pluginName)) - created, err := client.Resource.Create(ctx, pluginInstall, metav1.CreateOptions{}) - require.NoError(t, err) - require.NotNil(t, created) - require.Equal(t, pluginName, created.GetName()) - // Status should be empty as it's ignored on create - status, found, err := unstructured.NestedMap(created.Object, "status") - require.NoError(t, err) - require.True(t, found) // status field should exist - require.Empty(t, status) // but it should be empty - }) - t.Run("get plugin install", func(t *testing.T) { helper := setupHelper(t) ctx := context.Background() client := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, - GVR: gvrPluginInstalls, + GVR: gvrPlugins, }) pluginName := "test-plugin-get" - pluginInstall := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", + "kind": "Plugin", "metadata": {"name": "%s"}, "spec": {"version": "1.0.0"} }`, pluginName)) - created, err := client.Resource.Create(ctx, pluginInstall, metav1.CreateOptions{}) + created, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) require.NoError(t, err) fetched, err := client.Resource.Get(ctx, pluginName, metav1.GetOptions{}) require.NoError(t, err) @@ -111,16 +77,16 @@ func TestIntegrationPluginInstalls(t *testing.T) { ctx := context.Background() client := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, - GVR: gvrPluginInstalls, + GVR: gvrPlugins, }) pluginName := "test-plugin-update" - pluginInstall := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", + "kind": "Plugin", "metadata": {"name": "%s"}, "spec": {"version": "1.0.0"} }`, pluginName)) - created, err := client.Resource.Create(ctx, pluginInstall, metav1.CreateOptions{}) + created, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) require.NoError(t, err) updatedSpec := created.DeepCopy() updatedSpec.Object["spec"] = map[string]interface{}{ @@ -132,116 +98,21 @@ func TestIntegrationPluginInstalls(t *testing.T) { require.Equal(t, "2.0.0", updated.Object["spec"].(map[string]interface{})["version"]) }) - t.Run("update plugin install with status is ignored", func(t *testing.T) { - helper := setupHelper(t) - ctx := context.Background() - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrPluginInstalls, - }) - pluginName := "test-plugin-update-with-status" - pluginInstall := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", - "metadata": {"name": "%s"}, - "spec": {"version": "1.0.0"} - }`, pluginName)) - created, err := client.Resource.Create(ctx, pluginInstall, metav1.CreateOptions{}) - require.NoError(t, err) - - // Try to update the status via a normal update - withStatus := created.DeepCopy() - withStatus.Object["status"] = map[string]interface{}{ - "operatorStates": map[string]interface{}{ - "test-operator": map[string]interface{}{ - "lastEvaluation": "1", - "state": "success", - }, - }, - } - updated, err := client.Resource.Update(ctx, withStatus, metav1.UpdateOptions{}) - require.NoError(t, err) - require.NotNil(t, updated) - - // The status should not have been updated - status, found, err := unstructured.NestedMap(updated.Object, "status") - require.NoError(t, err) - require.True(t, found) - require.Empty(t, status) - - // also check with get - fetched, err := client.Resource.Get(ctx, pluginName, metav1.GetOptions{}) - require.NoError(t, err) - require.NotNil(t, fetched) - status, found, err = unstructured.NestedMap(fetched.Object, "status") - require.NoError(t, err) - require.True(t, found) - require.Empty(t, status) - }) - - t.Run("update plugin install status", func(t *testing.T) { - helper := setupHelper(t) - ctx := context.Background() - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrPluginInstalls, - }) - pluginName := "test-plugin-status" - pluginInstall := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", - "metadata": {"name": "%s"}, - "spec": {"version": "1.0.0"} - }`, pluginName)) - created, err := client.Resource.Create(ctx, pluginInstall, metav1.CreateOptions{}) - require.NoError(t, err) - - // Update the status - status := created.DeepCopy() - statusPayload := map[string]interface{}{ - "operatorStates": map[string]interface{}{ - "test-operator": map[string]interface{}{ - "lastEvaluation": "1", - "state": "success", - }, - }, - } - status.Object["status"] = statusPayload - updated, err := client.Resource.UpdateStatus(ctx, status, metav1.UpdateOptions{}) - require.NoError(t, err) - require.NotNil(t, updated) - - // Check the status on the returned object - actualStatus, found, err := unstructured.NestedMap(updated.Object, "status") - require.NoError(t, err) - require.True(t, found) - require.Equal(t, statusPayload, actualStatus) - - // Get the status to ensure it persisted - fetched, err := client.Resource.Get(ctx, pluginName, metav1.GetOptions{}) - require.NoError(t, err) - require.NotNil(t, fetched) - actualStatus, found, err = unstructured.NestedMap(fetched.Object, "status") - require.NoError(t, err) - require.True(t, found) - require.Equal(t, statusPayload, actualStatus) - }) - t.Run("list plugin installs", func(t *testing.T) { helper := setupHelper(t) ctx := context.Background() client := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, - GVR: gvrPluginInstalls, + GVR: gvrPlugins, }) pluginName := "test-plugin-list" - pluginInstall := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", + "kind": "Plugin", "metadata": {"name": "%s"}, "spec": {"version": "1.0.0"} }`, pluginName)) - created, err := client.Resource.Create(ctx, pluginInstall, metav1.CreateOptions{}) + created, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) require.NoError(t, err) list, err := client.Resource.List(ctx, metav1.ListOptions{}) require.NoError(t, err) @@ -254,16 +125,16 @@ func TestIntegrationPluginInstalls(t *testing.T) { ctx := context.Background() client := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, - GVR: gvrPluginInstalls, + GVR: gvrPlugins, }) pluginName := "test-plugin-delete" - pluginInstall := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", + "kind": "Plugin", "metadata": {"name": "%s"}, "spec": {"version": "1.0.0"} }`, pluginName)) - _, err := client.Resource.Create(ctx, pluginInstall, metav1.CreateOptions{}) + _, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) require.NoError(t, err) err = client.Resource.Delete(ctx, pluginName, metav1.DeleteOptions{}) require.NoError(t, err) @@ -281,15 +152,15 @@ func TestIntegrationPluginInstalls(t *testing.T) { t.Run(fmt.Sprintf("with basic role: %s", user.Identity.GetOrgRole()), func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ User: user, - GVR: gvrPluginInstalls, + GVR: gvrPlugins, }) - pluginInstall := helper.LoadYAMLOrJSON(`{ + plugin := helper.LoadYAMLOrJSON(`{ "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "PluginInstall", + "kind": "Plugin", "metadata": {"name": "test-plugin"}, "spec": {"version": "1.0.0"} }`) - _, err := client.Resource.Create(context.Background(), pluginInstall, metav1.CreateOptions{}) + _, err := client.Resource.Create(context.Background(), plugin, metav1.CreateOptions{}) statusError := helper.AsStatusError(err) require.Equal(t, metav1.StatusReasonForbidden, statusError.Status().Reason) err = client.Resource.Delete(context.Background(), "test-plugin", metav1.DeleteOptions{}) diff --git a/pkg/tests/apis/plugins/pluginsmeta_test.go b/pkg/tests/apis/plugins/pluginsmeta_test.go deleted file mode 100644 index 742dca3effc..00000000000 --- a/pkg/tests/apis/plugins/pluginsmeta_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package plugins - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/util/testutil" -) - -var gvrPluginMeta = schema.GroupVersionResource{ - Group: "plugins.grafana.app", - Version: "v0alpha1", - Resource: "pluginmetas", -} - -func TestIntegrationPluginMeta(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - t.Run("list plugin metas", func(t *testing.T) { - helper := setupHelper(t) - ctx := context.Background() - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrPluginMeta, - }) - list, err := client.Resource.List(ctx, metav1.ListOptions{}) - require.NoError(t, err) - require.NotNil(t, list) - require.Empty(t, list.Items) - }) - - t.Run("get plugin meta", func(t *testing.T) { - helper := setupHelper(t) - ctx := context.Background() - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrPluginMeta, - }) - _, err := client.Resource.Get(ctx, "example", metav1.GetOptions{}) - require.Error(t, err) - }) -} diff --git a/pkg/tests/apis/provisioning/job_validation_test.go b/pkg/tests/apis/provisioning/job_validation_test.go new file mode 100644 index 00000000000..c54ad58500c --- /dev/null +++ b/pkg/tests/apis/provisioning/job_validation_test.go @@ -0,0 +1,182 @@ +package provisioning + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_JobValidation(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + // Create a test repository first + const repo = "job-validation-test-repo" + testRepo := TestRepo{ + Name: repo, + Target: "instance", + Copies: map[string]string{}, + ExpectedDashboards: 0, + ExpectedFolders: 0, + } + helper.CreateRepo(t, testRepo) + + tests := []struct { + name string + jobSpec map[string]interface{} + expectedErr string + }{ + { + name: "job without action", + jobSpec: map[string]interface{}{ + "repository": repo, + }, + expectedErr: "spec.action: Required value: action must be specified", + }, + { + name: "job with invalid action", + jobSpec: map[string]interface{}{ + "action": "invalid-action", + "repository": repo, + }, + expectedErr: "spec.action: Invalid value: \"invalid-action\": invalid action", + }, + { + name: "pull job without pull options", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionPull), + "repository": repo, + }, + expectedErr: "spec.pull: Required value: pull options required for pull action", + }, + { + name: "push job without push options", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionPush), + "repository": repo, + }, + expectedErr: "spec.push: Required value: push options required for push action", + }, + { + name: "push job with invalid branch name", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionPush), + "repository": repo, + "push": map[string]interface{}{ + "branch": "feature..branch", // Invalid: consecutive dots + "message": "Test commit", + }, + }, + expectedErr: "spec.push.branch: Invalid value: \"feature..branch\": invalid git branch name", + }, + { + name: "push job with path traversal", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionPush), + "repository": repo, + "push": map[string]interface{}{ + "path": "../../etc/passwd", // Invalid: path traversal + "message": "Test commit", + }, + }, + expectedErr: "spec.push.path: Invalid value: \"../../etc/passwd\"", + }, + { + name: "delete job without paths or resources", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionDelete), + "repository": repo, + "delete": map[string]interface{}{}, + }, + expectedErr: "spec.delete: Required value: at least one path or resource must be specified", + }, + { + name: "delete job with invalid path", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionDelete), + "repository": repo, + "delete": map[string]interface{}{ + "paths": []string{"../invalid/path"}, + }, + }, + expectedErr: "spec.delete.paths[0]: Invalid value: \"../invalid/path\"", + }, + { + name: "move job without target path", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionMove), + "repository": repo, + "move": map[string]interface{}{ + "paths": []string{"dashboard.json"}, + }, + }, + expectedErr: "spec.move.targetPath: Required value: target path is required", + }, + { + name: "move job without paths or resources", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionMove), + "repository": repo, + "move": map[string]interface{}{ + "targetPath": "new-location/", + }, + }, + expectedErr: "spec.move: Required value: at least one path or resource must be specified", + }, + { + name: "move job with invalid target path", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionMove), + "repository": repo, + "move": map[string]interface{}{ + "paths": []string{"dashboard.json"}, + "targetPath": "../../../etc/", // Invalid: path traversal + }, + }, + expectedErr: "spec.move.targetPath: Invalid value: \"../../../etc/\"", + }, + { + name: "migrate job without migrate options", + jobSpec: map[string]interface{}{ + "action": string(provisioning.JobActionMigrate), + "repository": repo, + }, + expectedErr: "spec.migrate: Required value: migrate options required for migrate action", + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create the job object directly + jobObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Job", + "metadata": map[string]interface{}{ + "name": fmt.Sprintf("test-job-validation-%d", i), + "namespace": "default", + }, + "spec": tt.jobSpec, + }, + } + + // Try to create the job - should fail with validation error + _, err := helper.Jobs.Resource.Create(ctx, jobObj, metav1.CreateOptions{}) + require.Error(t, err, "expected validation error for invalid job spec") + + // Verify it's a validation error with correct status code + statusError := helper.RequireApiErrorStatus(err, metav1.StatusReasonInvalid, http.StatusUnprocessableEntity) + require.Contains(t, statusError.Message, tt.expectedErr, "error message should contain expected validation message") + }) + } +} diff --git a/pkg/tests/apis/provisioning/movejob_test.go b/pkg/tests/apis/provisioning/movejob_test.go index d7a6c4e7f44..85d227996fd 100644 --- a/pkg/tests/apis/provisioning/movejob_test.go +++ b/pkg/tests/apis/provisioning/movejob_test.go @@ -171,7 +171,7 @@ func TestIntegrationProvisioning_MoveJob(t *testing.T) { }) t.Run("move without target path", func(t *testing.T) { - // Create move job without target path (should fail) + // Create move job without target path (should fail validation at creation time) spec := provisioning.JobSpec{ Action: provisioning.JobActionMove, Move: &provisioning.MoveJobOptions{ @@ -180,9 +180,20 @@ func TestIntegrationProvisioning_MoveJob(t *testing.T) { }, } - job := helper.TriggerJobAndWaitForComplete(t, repo, spec) - state := mustNestedString(job.Object, "status", "state") - assert.Equal(t, "error", state, "move job should have failed due to missing target path") + // The job should be rejected by the admission controller with validation error + body := asJSON(&spec) + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(body). + SetHeader("Content-Type", "application/json"). + Do(ctx) + + require.Error(t, result.Error(), "move job without target path should fail validation") + statusError := helper.RequireApiErrorStatus(result.Error(), metav1.StatusReasonInvalid, 422) + require.Contains(t, statusError.Message, "spec.move.targetPath", "error should mention missing target path") }) t.Run("move by resource reference", func(t *testing.T) { diff --git a/pkg/tests/apis/provisioning/secrets_test.go b/pkg/tests/apis/provisioning/secrets_test.go index 6f0399e91f8..e4b55f5fa5f 100644 --- a/pkg/tests/apis/provisioning/secrets_test.go +++ b/pkg/tests/apis/provisioning/secrets_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/util/testutil" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -16,7 +17,9 @@ import ( func TestIntegrationProvisioning_InlineSecrets(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) - helper := runGrafana(t) + helper := runGrafana(t, func(opts *testinfra.GrafanaOpts) { + opts.SecretsManagerEnableDBMigrations = true + }) createOptions := metav1.CreateOptions{FieldValidation: "Strict"} ctx := context.Background() diff --git a/pkg/tests/apis/shorturl/shorturl_test.go b/pkg/tests/apis/shorturl/shorturl_test.go index 86445846acf..be7023c0c6a 100644 --- a/pkg/tests/apis/shorturl/shorturl_test.go +++ b/pkg/tests/apis/shorturl/shorturl_test.go @@ -5,13 +5,14 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" + shorturlV1 "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1beta1" "github.com/grafana/grafana/pkg/api/dtos" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/options" @@ -28,11 +29,7 @@ func TestMain(m *testing.M) { testsuite.Run(m) } -var gvr = schema.GroupVersionResource{ - Group: "shorturl.grafana.app", - Version: "v1alpha1", - Resource: "shorturls", -} +var gvr = shorturlV1.ShortURLKind().GroupVersionResource() var RESOURCEGROUP = gvr.GroupResource().String() @@ -78,29 +75,31 @@ func TestIntegrationShortURL(t *testing.T) { doLegacyOnlyTests(t, helper) }) - for _, mode := range []grafanarest.DualWriterMode{ - grafanarest.Mode1, - grafanarest.Mode2, - // grafanarest.Mode3, TODO: the /goto function needs to use an UpdateStatus client - // grafanarest.Mode4, - } { - t.Run(fmt.Sprintf("with dual write (unified storage, mode %d)", mode), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, - DisableAnonymous: true, - APIServerStorageType: options.StorageTypeUnified, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesShortURLs, - }, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - RESOURCEGROUP: { - DualWriterMode: mode, + t.Run("modes", func(t *testing.T) { + for _, mode := range []grafanarest.DualWriterMode{ + grafanarest.Mode1, + grafanarest.Mode2, + grafanarest.Mode3, + grafanarest.Mode4, + } { + t.Run(fmt.Sprintf("dual write (unified storage, mode %d)", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: options.StorageTypeUnified, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesShortURLs, }, - }, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + RESOURCEGROUP: { + DualWriterMode: mode, + }, + }, + }) + doDualWriteTests(t, helper, mode) }) - doDualWriteTests(t, helper, mode) - }) - } + } + }) t.Run("with dual write (unified storage, mode 5)", func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ @@ -173,7 +172,7 @@ func doLegacyOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { User: client.Args.User, Method: http.MethodGet, Path: "/goto/" + uid + "?orgId=default", - }, (*interface{})(nil)) + }, (*any)(nil)) assert.Equal(t, 302, redirectResponse.Response.StatusCode) }) } @@ -212,6 +211,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) require.NoError(t, err) assert.Equal(t, uid, found.GetName()) + assert.LessOrEqual(t, time.Since(found.GetCreationTimestamp().Time).Seconds(), 30.0, "creation timestamp should be within last 30 seconds") // Verify cross-API consistency getFromBothAPIs(t, helper, client, uid) @@ -231,7 +231,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest obj := apis.DoRequest(helper, apis.RequestParams{ User: client.Args.User, Method: http.MethodPost, - Path: "/apis/shorturl.grafana.app/v1alpha1/namespaces/default/shorturls", + Path: "/apis/shorturl.grafana.app/v1beta1/namespaces/default/shorturls", Body: []byte(`{ "metadata": { "generateName": "test-" }, "spec": { "path": "d/xCmMwXdVz/k8s-dual-write" } }`), }, &unstructured.Unstructured{}) require.NotNil(t, obj.Result) @@ -257,6 +257,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest }) t.Run("Redirect functionality", func(t *testing.T) { + t.Skip("Skipping redirect functionality tests for now - flaky test") client := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Editor, GVR: gvr, @@ -266,7 +267,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest obj := apis.DoRequest(helper, apis.RequestParams{ User: client.Args.User, Method: http.MethodPost, - Path: "/apis/shorturl.grafana.app/v1alpha1/namespaces/default/shorturls", + Path: "/apis/shorturl.grafana.app/v1beta1/namespaces/default/shorturls", Body: []byte(`{ "metadata": { "generateName": "redirect-" }, "spec": { "path": "d/test/redirect" } }`), }, &unstructured.Unstructured{}) require.NotNil(t, obj.Result) @@ -278,21 +279,23 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest User: client.Args.User, Method: http.MethodGet, Path: "/goto/" + uid + "?orgId=default", - }, (*interface{})(nil)) + }, (*any)(nil)) assert.Equal(t, 302, redirectResponse.Response.StatusCode) - // Verify lastSeenAt was updated (should be > 0 now) - found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) - require.NoError(t, err) - status, exists := found.Object["status"].(map[string]interface{}) - assert.True(t, exists) - lastSeenAt, exists := status["lastSeenAt"].(int64) - assert.True(t, exists) + require.EventuallyWithT(t, func(t *assert.CollectT) { + // Verify lastSeenAt was updated (should be > 0 now) + found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{}) + require.NoError(t, err) - assert.Greater(t, lastSeenAt, int64(0)) + lastSeenAt, exists, err := unstructured.NestedInt64(found.Object, "status", "lastSeenAt") + require.NoError(t, err) + require.True(t, exists) + + require.Greater(t, lastSeenAt, int64(1), "lastSeenAt should be greater than 1 after redirect") + }, time.Second*15, time.Millisecond*150, "lastSeenAt not changed after 15s") // Clean up - err = client.Resource.Delete(context.Background(), uid, metav1.DeleteOptions{}) + err := client.Resource.Delete(context.Background(), uid, metav1.DeleteOptions{}) require.NoError(t, err) }) } @@ -317,7 +320,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { obj := apis.DoRequest(helper, apis.RequestParams{ User: client.Args.User, Method: http.MethodPost, - Path: "/apis/shorturl.grafana.app/v1alpha1/namespaces/default/shorturls", + Path: "/apis/shorturl.grafana.app/v1beta1/namespaces/default/shorturls", Body: []byte(`{ "metadata": { "generateName": "unified-" }, "spec": { "path": "d/xCmMwXdVz/unified-only" } }`), }, &unstructured.Unstructured{}) require.NotNil(t, obj.Result) @@ -379,7 +382,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { response := apis.DoRequest(helper, apis.RequestParams{ User: client.Args.User, Method: http.MethodPost, - Path: "/apis/shorturl.grafana.app/v1alpha1/namespaces/default/shorturls", + Path: "/apis/shorturl.grafana.app/v1beta1/namespaces/default/shorturls", Body: []byte(invalidBody), }, (*unstructured.Unstructured)(nil)) @@ -416,7 +419,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { response := apis.DoRequest(helper, apis.RequestParams{ User: client.Args.User, Method: http.MethodPost, - Path: "/apis/shorturl.grafana.app/v1alpha1/namespaces/default/shorturls", + Path: "/apis/shorturl.grafana.app/v1beta1/namespaces/default/shorturls", Body: []byte(validBody), }, &unstructured.Unstructured{}) @@ -446,7 +449,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { obj := apis.DoRequest[unstructured.Unstructured](helper, apis.RequestParams{ User: client.Args.User, Method: http.MethodPost, - Path: "/apis/shorturl.grafana.app/v1alpha1/namespaces/default/shorturls", + Path: "/apis/shorturl.grafana.app/v1beta1/namespaces/default/shorturls", Body: []byte(`{ "metadata": { "generateName": "redirect-unified-" }, "spec": { "path": "d/test/unified-redirect" } }`), }, &unstructured.Unstructured{}) require.NotNil(t, obj.Result) @@ -458,7 +461,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { User: client.Args.User, Method: http.MethodGet, Path: "/goto/" + uid + "?orgId=default", - }, (*interface{})(nil)) + }, (*any)(nil)) assert.Equal(t, 302, redirectResponse.Response.StatusCode) // Clean up @@ -505,9 +508,9 @@ func getFromBothAPIs(t *testing.T, if legacyShortURL != nil { // If legacy API returns data, verify consistency - spec, ok := k8sResource.Object["spec"].(map[string]interface{}) + spec, ok := k8sResource.Object["spec"].(map[string]any) require.True(t, ok) - status, ok := k8sResource.Object["status"].(map[string]interface{}) + status, ok := k8sResource.Object["status"].(map[string]any) require.True(t, ok) assert.Equal(t, legacyShortURL.Uid, k8sResource.GetName()) assert.Equal(t, legacyShortURL.Path, spec["path"].(string)) diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index b680cae8060..8f00449ac4b 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -552,6 +552,13 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { require.NoError(t, err) } + if opts.SecretsManagerEnableDBMigrations { + apiserverSection, err := getOrCreateSection("secrets_manager") + require.NoError(t, err) + _, err = apiserverSection.NewKey("run_secrets_db_migrations", "true") + require.NoError(t, err) + } + dashboardsSection, err := getOrCreateSection("dashboards") require.NoError(t, err) _, err = dashboardsSection.NewKey("min_refresh_interval", "10s") @@ -623,6 +630,7 @@ type GrafanaOpts struct { EnableSCIM bool APIServerRuntimeConfig string DisableControllers bool + SecretsManagerEnableDBMigrations bool // When "unified-grpc" is selected it will also start the grpc server APIServerStorageType options.StorageType diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 1015393477b..7272c08836e 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "slices" + "sync" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -36,7 +37,7 @@ const ( headerFromAlert = "FromAlert" defaultRegion = "default" - logsQueryMode = "Logs" + queryModeLogs = "Logs" // QueryTypes annotationQuery = "annotationQuery" logAction = "logAction" @@ -45,7 +46,8 @@ const ( type DataQueryJson struct { dataquery.CloudWatchAnnotationQuery - Type string `json:"type,omitempty"` + Type string `json:"type,omitempty"` + LogsMode dataquery.LogsMode `json:"logsMode,omitempty"` } type DataSource struct { @@ -53,9 +55,10 @@ type DataSource struct { ProxyOpts *proxy.Options AWSConfigProvider awsauth.ConfigProvider - logger log.Logger - tagValueCache *cache.Cache - resourceHandler backend.CallResourceHandler + logger log.Logger + tagValueCache *cache.Cache + resourceHandler backend.CallResourceHandler + monitoringAccountCache sync.Map } func (ds *DataSource) newAWSConfig(ctx context.Context, region string) (aws.Config, error) { @@ -147,12 +150,22 @@ func (ds *DataSource) QueryData(ctx context.Context, req *backend.QueryDataReque if model.QueryMode != "" { queryMode = string(model.QueryMode) } - fromPublicDashboard := model.Type == "" && queryMode == logsQueryMode - isSyncLogQuery := ((fromAlert || fromExpression) && queryMode == logsQueryMode) || fromPublicDashboard + + fromPublicDashboard := model.Type == "" + + isLogInsightsQuery := queryMode == queryModeLogs && (model.LogsMode == "" || model.LogsMode == dataquery.LogsModeInsights) + + isSyncLogQuery := isLogInsightsQuery && ((fromAlert || fromExpression) || fromPublicDashboard) + if isSyncLogQuery { return executeSyncLogQuery(ctx, ds, req) } + isLogsAnomaliesQuery := model.QueryMode == dataquery.CloudWatchQueryModeLogs && model.LogsMode == dataquery.LogsModeAnomalies + if isLogsAnomaliesQuery { + return executeLogAnomaliesQuery(ctx, ds, req) + } + var result *backend.QueryDataResponse switch model.Type { case annotationQuery: @@ -262,6 +275,33 @@ func (ds *DataSource) getRGTAClient(ctx context.Context, region string) (resourc return NewRGTAClient(cfg), nil } +func (ds *DataSource) isMonitoringAccount(ctx context.Context, region string) (bool, error) { + if value, ok := ds.monitoringAccountCache.Load(region); ok { + cached := value.(bool) + return cached, nil + } + + client, err := ds.GetAccountsService(ctx, region) + if err != nil { + return false, err + } + + accounts, err := client.GetAccountsForCurrentUserOrRole(ctx) + if err != nil { + return false, err + } + + for _, account := range accounts { + if account.Value.IsMonitoringAccount { + ds.monitoringAccountCache.Store(region, true) + return true, nil + } + } + + ds.monitoringAccountCache.Store(region, false) + return false, nil +} + var terminatedStates = []cloudwatchlogstypes.QueryStatus{ cloudwatchlogstypes.QueryStatusComplete, cloudwatchlogstypes.QueryStatusCancelled, diff --git a/pkg/tsdb/cloudwatch/constants/metrics.go b/pkg/tsdb/cloudwatch/constants/metrics.go index f52207001ec..02da8f26a7a 100644 --- a/pkg/tsdb/cloudwatch/constants/metrics.go +++ b/pkg/tsdb/cloudwatch/constants/metrics.go @@ -17,6 +17,7 @@ func Regions() RegionsSet { "ap-southeast-3": {}, "ap-southeast-4": {}, "ap-southeast-5": {}, + "ap-southeast-6": {}, "ap-southeast-7": {}, "ca-central-1": {}, "ca-west-1": {}, @@ -24,6 +25,7 @@ func Regions() RegionsSet { "cn-northwest-1": {}, "eu-central-1": {}, "eu-central-2": {}, + "eu-isoe-west-1": {}, "eu-north-1": {}, "eu-south-1": {}, "eu-south-2": {}, @@ -40,7 +42,11 @@ func Regions() RegionsSet { "us-gov-east-1": {}, "us-gov-west-1": {}, "us-iso-east-1": {}, + "us-iso-west-1": {}, "us-isob-east-1": {}, + "us-isob-west-1": {}, + "us-isof-east-1": {}, + "us-northeast-1": {}, "us-west-1": {}, "us-west-2": {}, } diff --git a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go index 76c706094a6..7d943a71130 100644 --- a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go @@ -285,6 +285,13 @@ func NewQueryEditorOperatorValueType() *QueryEditorOperatorValueType { return NewStringOrBoolOrInt64OrArrayOfQueryEditorOperatorType() } +type LogsMode string + +const ( + LogsModeInsights LogsMode = "Insights" + LogsModeAnomalies LogsMode = "Anomalies" +) + type LogsQueryLanguage string const ( @@ -297,7 +304,9 @@ const ( type CloudWatchLogsQuery struct { // Whether a query is a Metrics, Logs, or Annotations query QueryMode CloudWatchQueryMode `json:"queryMode"` - Id string `json:"id"` + // Whether a query is a Logs Insights or Log Anomalies query + LogsMode *LogsMode `json:"logsMode,omitempty"` + Id string `json:"id"` // AWS region to query for the logs Region string `json:"region"` // The CloudWatch Logs Insights query to execute @@ -347,6 +356,40 @@ func NewLogGroup() *LogGroup { return &LogGroup{} } +// Shape of a Cloudwatch Log Anomalies query +type CloudWatchLogsAnomaliesQuery struct { + Id string `json:"id"` + // AWS region to query for the logs + Region string `json:"region"` + // Whether a query is a Metrics, Logs or Annotations query + QueryMode *CloudWatchQueryMode `json:"queryMode,omitempty"` + // Whether a query is a Logs Insights or Log Anomalies query + LogsMode *LogsMode `json:"logsMode,omitempty"` + // Filter to return only anomalies that are 'SUPPRESSED', 'UNSUPPRESSED', or 'ALL' (default) + SuppressionState *string `json:"suppressionState,omitempty"` + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. + RefId string `json:"refId"` + // If hide is set to true, Grafana will filter out the response(s) associated with this query before returning it to the panel. + Hide *bool `json:"hide,omitempty"` + // Specify the query flavor + // TODO make this required and give it a default + QueryType *string `json:"queryType,omitempty"` + // Used to filter only the anomalies found by a certain anomaly detector + AnomalyDetectionARN *string `json:"anomalyDetectionARN,omitempty"` + // For mixed data sources the selected datasource is on the query level. + // For non mixed scenarios this is undefined. + // TODO find a better way to do this ^ that's friendly to schema + // TODO this shouldn't be unknown but DataSourceRef | null + Datasource any `json:"datasource,omitempty"` +} + +// NewCloudWatchLogsAnomaliesQuery creates a new CloudWatchLogsAnomaliesQuery object. +func NewCloudWatchLogsAnomaliesQuery() *CloudWatchLogsAnomaliesQuery { + return &CloudWatchLogsAnomaliesQuery{} +} + // Shape of a CloudWatch Annotation query // TS type is CloudWatchDefaultQuery = Omit & CloudWatchMetricsQuery, declared in veneer // #CloudWatchDefaultQuery: #CloudWatchLogsQuery & #CloudWatchMetricsQuery @cuetsy(kind="type") diff --git a/pkg/tsdb/cloudwatch/log_actions.go b/pkg/tsdb/cloudwatch/log_actions.go index 8a6022d44fa..60ebad06442 100644 --- a/pkg/tsdb/cloudwatch/log_actions.go +++ b/pkg/tsdb/cloudwatch/log_actions.go @@ -213,17 +213,48 @@ func (ds *DataSource) executeStartQuery(ctx context.Context, logsClient models.C // log group identifiers can be left out if the query is an SQL query if *logsQuery.QueryLanguage != dataquery.LogsQueryLanguageSQL { - if len(logsQuery.LogGroups) > 0 && features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying) { - var logGroupIdentifiers []string - for _, lg := range logsQuery.LogGroups { - arn := lg.Arn - // due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error - logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*")) + useLogGroupIdentifiers := false + logGroupsFromQuery := len(logsQuery.LogGroups) > 0 + if logGroupsFromQuery && features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying) { + region := logsQuery.Region + if region == "" || region == defaultRegion { + region = ds.Settings.Region + } + if region != "" { + isMonitoringAccount, err := ds.isMonitoringAccount(ctx, region) + if err != nil { + ds.logger.FromContext(ctx).Debug("failed to determine monitoring account status", "err", err) + } else if isMonitoringAccount { + // monitoring accounts require querying by log group identifiers because log group names are not unique across accounts. + var logGroupIdentifiers []string + for _, lg := range logsQuery.LogGroups { + // due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error + arn := strings.TrimSuffix(lg.Arn, "*") + logGroupIdentifiers = append(logGroupIdentifiers, arn) + } + startQueryInput.LogGroupIdentifiers = logGroupIdentifiers + useLogGroupIdentifiers = true + } + } + } + + if !useLogGroupIdentifiers { + // even though logsQuery.LogGroupNames is deprecated, we still need to support it for backwards compatibility and alert queries + startQueryInput.LogGroupNames = append([]string(nil), logsQuery.LogGroupNames...) + if len(startQueryInput.LogGroupNames) == 0 && logGroupsFromQuery { + // deduplicate log group names because we only deduplicate log groups by their ARNs instead of their names when the query is created + seenLogGroupNames := make(map[string]struct{}, len(logsQuery.LogGroups)) + for _, lg := range logsQuery.LogGroups { + if lg.Name == "" { + continue + } + if _, exists := seenLogGroupNames[lg.Name]; exists { + continue + } + seenLogGroupNames[lg.Name] = struct{}{} + startQueryInput.LogGroupNames = append(startQueryInput.LogGroupNames, lg.Name) + } } - startQueryInput.LogGroupIdentifiers = logGroupIdentifiers - } else { - // even though log group names are being phased out, we still need to support them for backwards compatibility and alert queries - startQueryInput.LogGroupNames = logsQuery.LogGroupNames } } diff --git a/pkg/tsdb/cloudwatch/log_actions_test.go b/pkg/tsdb/cloudwatch/log_actions_test.go index ae1a79434bd..142b1d0e24d 100644 --- a/pkg/tsdb/cloudwatch/log_actions_test.go +++ b/pkg/tsdb/cloudwatch/log_actions_test.go @@ -445,7 +445,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled", func(t *testing.T) { cli = fakeCWLogsClient{} - ds := newTestDatasource() + ds := newTestDatasource(func(ds *DataSource) { + ds.monitoringAccountCache.Store("us-east-1", true) + }) _, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -459,7 +461,8 @@ func Test_executeStartQuery(t *testing.T) { "limit": 12, "queryLanguage": "CWLI", "queryString":"fields @message", - "logGroups":[{"arn": "fakeARN"}] + "logGroups":[{"arn": "fakeARN"}], + "region": "us-east-1" }`), }, }, @@ -480,7 +483,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled and strips out trailing *", func(t *testing.T) { cli = fakeCWLogsClient{} - ds := newTestDatasource() + ds := newTestDatasource(func(ds *DataSource) { + ds.monitoringAccountCache.Store("us-east-1", true) + }) _, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -493,7 +498,8 @@ func Test_executeStartQuery(t *testing.T) { "subtype": "StartQuery", "limit": 12, "queryString":"fields @message", - "logGroups":[{"arn": "*fake**ARN*"}] + "logGroups":[{"arn": "*fake**ARN*"}], + "region": "us-east-1" }`), }, }, @@ -512,6 +518,44 @@ func Test_executeStartQuery(t *testing.T) { }, cli.calls.startQuery) }) + t.Run("queries by LogGroupNames on StartQueryInput when queried region is not a monitoring account region for the data source", func(t *testing.T) { + cli = fakeCWLogsClient{} + ds := newTestDatasource(func(ds *DataSource) { + // note that the query's region is set to us-east-2, but the data source is only a monitoring account in us-east-1 so it should query by LogGroupNames + ds.monitoringAccountCache.Store("us-east-1", true) + }) + + _, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "type": "logAction", + "subtype": "StartQuery", + "limit": 12, + "queryString":"fields @message", + "logGroups":[{"arn": "arn:aws:logs:us-east-1:123456789012:log-group:group","name":"/log-group"}], + "region": "us-east-2" + }`), + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, []*cloudwatchlogs.StartQueryInput{ + { + StartTime: aws.Int64(0), + EndTime: aws.Int64(1), + Limit: aws.Int32(12), + QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"), + LogGroupNames: []string{"/log-group"}, + QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli, + }, + }, cli.calls.startQuery) + }) + t.Run("uses LogGroupNames if the cross account feature flag is not enabled, and log group names is present", func(t *testing.T) { cli = fakeCWLogsClient{} ds := newTestDatasource() @@ -545,6 +589,42 @@ func Test_executeStartQuery(t *testing.T) { }, cli.calls.startQuery) }) + t.Run("deduplicates log group names when derived from logGroups", func(t *testing.T) { + cli = fakeCWLogsClient{} + ds := newTestDatasource() + + _, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "type": "logAction", + "subtype": "StartQuery", + "limit": 12, + "queryString":"fields @message", + "logGroups":[ + {"arn": "arn:aws:logs:us-east-1:123456789012:log-group:group1","name":"/log-group"}, + {"arn": "arn:aws:logs:us-east-1:123456789012:log-group:group2","name":"/log-group"} + ] + }`), + }, + }, + }) + assert.NoError(t, err) + assert.Equal(t, []*cloudwatchlogs.StartQueryInput{ + { + StartTime: aws.Int64(0), + EndTime: aws.Int64(1), + Limit: aws.Int32(12), + QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"), + LogGroupNames: []string{"/log-group"}, + QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli, + }, + }, cli.calls.startQuery) + }) + t.Run("ignores logGroups if feature flag is disabled even if logGroupNames is not present", func(t *testing.T) { cli = fakeCWLogsClient{} ds := newTestDatasource() @@ -600,12 +680,12 @@ func Test_executeStartQuery(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []*cloudwatchlogs.StartQueryInput{ { - StartTime: aws.Int64(0), - EndTime: aws.Int64(1), - Limit: aws.Int32(12), - QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"), - LogGroupIdentifiers: []string{"*fake**ARN"}, - QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli, + StartTime: aws.Int64(0), + EndTime: aws.Int64(1), + Limit: aws.Int32(12), + QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"), + LogGroupNames: []string{"/log-group"}, + QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli, }, }, cli.calls.startQuery) }) diff --git a/pkg/tsdb/cloudwatch/log_anomalies_query.go b/pkg/tsdb/cloudwatch/log_anomalies_query.go new file mode 100644 index 00000000000..f39b92eeffa --- /dev/null +++ b/pkg/tsdb/cloudwatch/log_anomalies_query.go @@ -0,0 +1,146 @@ +package cloudwatch + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + + cloudwatchLogsTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery" +) + +var executeLogAnomaliesQuery = func(ctx context.Context, ds *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + resp := backend.NewQueryDataResponse() + + for _, q := range req.Queries { + var anomaliesQuery dataquery.CloudWatchLogsAnomaliesQuery + err := json.Unmarshal(q.JSON, &anomaliesQuery) + if err != nil { + continue + } + + region := anomaliesQuery.Region + if region == "" || region == defaultRegion { + anomaliesQuery.Region = ds.Settings.Region + } + + logsClient, err := ds.getCWLogsClient(ctx, region) + if err != nil { + return nil, err + } + + listAnomaliesInput := &cloudwatchlogs.ListAnomaliesInput{} + if anomaliesQuery.SuppressionState != nil { + listAnomaliesInput.SuppressionState = getSuppressionState(*anomaliesQuery.SuppressionState) + } + + if anomaliesQuery.AnomalyDetectionARN == nil || *anomaliesQuery.AnomalyDetectionARN != "" { + listAnomaliesInput.AnomalyDetectorArn = anomaliesQuery.AnomalyDetectionARN + } + + response, err := logsClient.ListAnomalies(ctx, listAnomaliesInput) + + if err != nil { + result := backend.NewQueryDataResponse() + result.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("%v: %w", "failed to call cloudwatch:ListAnomalies", err))) + return result, nil + } + + dataframe, err := logsAnomaliesResultsToDataframes(response) + if err != nil { + return nil, err + } + + respD := resp.Responses[q.RefID] + respD.Frames = data.Frames{dataframe} + resp.Responses[q.RefID] = respD + } + + return resp, nil + +} + +func logsAnomaliesResultsToDataframes(response *cloudwatchlogs.ListAnomaliesOutput) (*data.Frame, error) { + frame := data.NewFrame("Log anomalies") + + if len(response.Anomalies) == 0 { + return frame, nil + } + + n := len(response.Anomalies) + anomalyArns := make([]string, n) + descriptions := make([]string, n) + suppressedStatus := make([]bool, n) + + priorities := make([]string, n) + patterns := make([]string, n) + statuses := make([]string, n) + logGroupArnLists := make([]string, n) + firstSeens := make([]time.Time, n) + lastSeens := make([]time.Time, n) + logTrends := make([]*json.RawMessage, n) + + for i, anomaly := range response.Anomalies { + anomalyArns[i] = *anomaly.AnomalyDetectorArn + descriptions[i] = *anomaly.Description + suppressedStatus[i] = *anomaly.Suppressed + priorities[i] = *anomaly.Priority + if anomaly.PatternString != nil { + patterns[i] = *anomaly.PatternString + } + statuses[i] = string(anomaly.State) + logGroupArnLists[i] = strings.Join(anomaly.LogGroupArnList, ",") + + firstSeens[i] = time.UnixMilli(anomaly.FirstSeen) + + lastSeens[i] = time.UnixMilli(anomaly.LastSeen) + + // data.Frame returned from the backend cannot contain fields of type data.Frames + // so histogram is kept as json.RawMessageto be built as sparkline table cell on the FE + histogramField := anomaly.Histogram + histogramJSON, err := json.Marshal(histogramField) + if err != nil { + logTrends[i] = nil + } else { + rawMsg := json.RawMessage(histogramJSON) + logTrends[i] = &rawMsg + } + } + + newFields := make([]*data.Field, 0, len(response.Anomalies)) + + newFields = append(newFields, data.NewField("state", nil, statuses).SetConfig(&data.FieldConfig{DisplayName: "State"})) + newFields = append(newFields, data.NewField("description", nil, descriptions).SetConfig(&data.FieldConfig{DisplayName: "Anomaly"})) + newFields = append(newFields, data.NewField("priority", nil, priorities).SetConfig(&data.FieldConfig{DisplayName: "Priority"})) + newFields = append(newFields, data.NewField("patternString", nil, patterns).SetConfig(&data.FieldConfig{DisplayName: "Log Pattern"})) + // FE expects the field name to be logTrend in order to identify histogram field for sparkline rendering + newFields = append(newFields, data.NewField("logTrend", nil, logTrends).SetConfig(&data.FieldConfig{DisplayName: "Log Trend"})) + newFields = append(newFields, data.NewField("firstSeen", nil, firstSeens).SetConfig(&data.FieldConfig{DisplayName: "First seen"})) + newFields = append(newFields, data.NewField("lastSeen", nil, lastSeens).SetConfig(&data.FieldConfig{DisplayName: "Last seen"})) + newFields = append(newFields, data.NewField("suppressed", nil, suppressedStatus).SetConfig(&data.FieldConfig{DisplayName: "Suppressed?"})) + newFields = append(newFields, data.NewField("logGroupArnList", nil, logGroupArnLists).SetConfig(&data.FieldConfig{DisplayName: "Log Groups"})) + newFields = append(newFields, data.NewField("anomalyArn", nil, anomalyArns).SetConfig(&data.FieldConfig{DisplayName: "Anomaly Arn"})) + + frame.Fields = newFields + setPreferredVisType(frame, data.VisTypeTable) + return frame, nil +} + +func getSuppressionState(suppressionState string) cloudwatchLogsTypes.SuppressionState { + switch suppressionState { + case "suppressed": + return cloudwatchLogsTypes.SuppressionStateSuppressed + case "unsuppressed": + return cloudwatchLogsTypes.SuppressionStateUnsuppressed + case "all": + return "" + default: + return "" + } +} diff --git a/pkg/tsdb/cloudwatch/log_anomalies_query_test.go b/pkg/tsdb/cloudwatch/log_anomalies_query_test.go new file mode 100644 index 00000000000..d9b3b85314d --- /dev/null +++ b/pkg/tsdb/cloudwatch/log_anomalies_query_test.go @@ -0,0 +1,212 @@ +package cloudwatch + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + cloudwatchLogsTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" + "github.com/stretchr/testify/assert" +) + +func Test_executeLogAnomaliesQuery(t *testing.T) { + origNewCWClient := NewCWClient + t.Cleanup(func() { + NewCWClient = origNewCWClient + }) + + var cli fakeCWLogsClient + NewCWLogsClient = func(aws.Config) models.CWLogsClient { + return &cli + } + + t.Run("getCWLogsClient is called with correct suppression state", func(t *testing.T) { + testcases := []struct { + name string + suppressionStateInQuery string + result cloudwatchLogsTypes.SuppressionState + }{ + { + "suppressed state", + "suppressed", + cloudwatchLogsTypes.SuppressionStateSuppressed, + }, + { + "unsuppressed state", + "unsuppressed", + cloudwatchLogsTypes.SuppressionStateUnsuppressed, + }, + { + "empty state", + "", + "", + }, + { + "all state", + "all", + "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + cli = fakeCWLogsClient{anomalies: []cloudwatchLogsTypes.Anomaly{}} + ds := newTestDatasource() + + _, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{ + Headers: map[string]string{headerFromAlert: ""}, + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "queryMode": "Logs", + "logsMode": "Anomalies", + "suppressionState": "` + tc.suppressionStateInQuery + `", + "region": "us-east-1" + }`), + }, + }, + }) + assert.NoError(t, err) + assert.Equal(t, tc.result, cli.calls.listAnomalies[0].SuppressionState) + }) + } + }) +} + +func Test_executeLogAnomaliesQuery_returns_data_frames(t *testing.T) { + origNewCWClient := NewCWClient + t.Cleanup(func() { + NewCWClient = origNewCWClient + }) + + var cli fakeCWLogsClient + NewCWLogsClient = func(aws.Config) models.CWLogsClient { + return &cli + } + t.Run("returns log anomalies data frames", func(t *testing.T) { + cli = fakeCWLogsClient{anomalies: []cloudwatchLogsTypes.Anomaly{ + { + AnomalyId: aws.String("anomaly-1"), + AnomalyDetectorArn: aws.String("arn:aws:logs:us-east-1:123456789012:anomaly-detector:anomaly-detector-1"), + FirstSeen: 1622505600000, // June 1, 2021 00:00:00 GMT + LastSeen: 1622592000000, // June 2, 2021 00:00:00 GMT + LogGroupArnList: []string{"arn:aws:logs:us-east-1:1234567:log-group-1:id-1", "arn:aws:logs:us-east-1:1234567:log-group-2:id-2"}, + Description: aws.String("Description 1"), + State: cloudwatchLogsTypes.StateActive, + Priority: aws.String("high"), + PatternString: aws.String(`{"ClusterName":"PetSite","Namespace":"default","Service":"service-petsite",,"instance":"instance"-5:Token-6,"job":"kubernetes-service-endpoints","pod_name":"pod_name"-9,"prom_metric_type":"counter"}`), + Suppressed: aws.Bool(false), + Histogram: map[string]int64{ + "1622505600000": 5, + "1622519200000": 10, + "1622532800000": 7, + }, + }, + { + AnomalyId: aws.String("anomaly-2"), + AnomalyDetectorArn: aws.String("arn:aws:logs:us-east-1:123456789012:anomaly-detector:anomaly-detector-2"), + FirstSeen: 1622592000000, // June 2, 2021 00:00:00 GMT + LastSeen: 1622678400000, // June 3, 2021 00:00:00 GMT + LogGroupArnList: []string{"arn:aws:logs:us-east-1:1234567:log-group-1:id-3", "arn:aws:logs:us-east-1:1234567:log-group-2:id-4"}, + Description: aws.String("Description 2"), + State: cloudwatchLogsTypes.StateSuppressed, + Priority: aws.String("low"), + PatternString: aws.String(`{"ClusterName":"PetSite","Namespace":"default","Service":"service-petsite","dotnet_collection_count_total":"dotnet_collection_count_total"-3}`), + Suppressed: aws.Bool(true), + Histogram: map[string]int64{ + "1622592000000": 3, + }, + }, + }} + + ds := newTestDatasource() + + resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{ + Headers: map[string]string{headerFromAlert: ""}, + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + TimeRange: backend.TimeRange{From: time.Unix(0, 0), To: time.Unix(1, 0)}, + JSON: json.RawMessage(`{ + "queryMode": "Logs", + "logsMode": "Anomalies", + "suppressionState": "all", + "region": "us-east-1" + }`), + }, + }, + }) + + assert.NoError(t, err) + assert.Len(t, resp.Responses, 1) + for _, r := range resp.Responses { + assert.Len(t, r.Frames, 1) + frame := r.Frames[0] + assert.Equal(t, "Log anomalies", frame.Name) + assert.Len(t, frame.Fields, 10) + + stateField := frame.Fields[0] + assert.Equal(t, "state", stateField.Name) + assert.Equal(t, "Active", stateField.At(0)) + assert.Equal(t, "Suppressed", stateField.At(1)) + + descriptionField := frame.Fields[1] + assert.Equal(t, "description", descriptionField.Name) + assert.Equal(t, "Description 1", descriptionField.At(0)) + assert.Equal(t, "Description 2", descriptionField.At(1)) + + priorityField := frame.Fields[2] + assert.Equal(t, "priority", priorityField.Name) + assert.Equal(t, "high", priorityField.At(0)) + assert.Equal(t, "low", priorityField.At(1)) + + patternStringField := frame.Fields[3] + assert.Equal(t, "patternString", patternStringField.Name) + assert.Equal(t, `{"ClusterName":"PetSite","Namespace":"default","Service":"service-petsite",,"instance":"instance"-5:Token-6,"job":"kubernetes-service-endpoints","pod_name":"pod_name"-9,"prom_metric_type":"counter"}`, patternStringField.At(0)) + assert.Equal(t, `{"ClusterName":"PetSite","Namespace":"default","Service":"service-petsite","dotnet_collection_count_total":"dotnet_collection_count_total"-3}`, patternStringField.At(1)) + + histogramField := frame.Fields[4] + assert.Equal(t, "logTrend", histogramField.Name) + + histogram0 := histogramField.At(0).(*json.RawMessage) + var histData0 map[string]int64 + err = json.Unmarshal(*histogram0, &histData0) + assert.NoError(t, err) + assert.Equal(t, int64(5), histData0["1622505600000"]) + assert.Equal(t, int64(10), histData0["1622519200000"]) + assert.Equal(t, int64(7), histData0["1622532800000"]) + + firstSeenField := frame.Fields[5] + assert.Equal(t, "firstSeen", firstSeenField.Name) + assert.Equal(t, time.Unix(1622505600, 0), firstSeenField.At(0)) + assert.Equal(t, time.Unix(1622592000, 0), firstSeenField.At(1)) + + lastSeenField := frame.Fields[6] + assert.Equal(t, "lastSeen", lastSeenField.Name) + assert.Equal(t, time.Unix(1622592000, 0), lastSeenField.At(0)) + assert.Equal(t, time.Unix(1622678400, 0), lastSeenField.At(1)) + + suppressedField := frame.Fields[7] + assert.Equal(t, "suppressed", suppressedField.Name) + assert.Equal(t, false, suppressedField.At(0)) + assert.Equal(t, true, suppressedField.At(1)) + + logGroupArnListField := frame.Fields[8] + assert.Equal(t, "logGroupArnList", logGroupArnListField.Name) + assert.Equal(t, "arn:aws:logs:us-east-1:1234567:log-group-1:id-1,arn:aws:logs:us-east-1:1234567:log-group-2:id-2", logGroupArnListField.At(0)) + assert.Equal(t, "arn:aws:logs:us-east-1:1234567:log-group-1:id-3,arn:aws:logs:us-east-1:1234567:log-group-2:id-4", logGroupArnListField.At(1)) + + anomalyDetectorArnField := frame.Fields[9] + assert.Equal(t, "anomalyArn", anomalyDetectorArnField.Name) + assert.Equal(t, "arn:aws:logs:us-east-1:123456789012:anomaly-detector:anomaly-detector-1", anomalyDetectorArnField.At(0)) + assert.Equal(t, "arn:aws:logs:us-east-1:123456789012:anomaly-detector:anomaly-detector-2", anomalyDetectorArnField.At(1)) + } + }) +} diff --git a/pkg/tsdb/cloudwatch/log_sync_query_test.go b/pkg/tsdb/cloudwatch/log_sync_query_test.go index b1c0b99175e..376464d9340 100644 --- a/pkg/tsdb/cloudwatch/log_sync_query_test.go +++ b/pkg/tsdb/cloudwatch/log_sync_query_test.go @@ -137,7 +137,7 @@ func Test_executeSyncLogQuery(t *testing.T) { executeSyncLogQuery = origExecuteSyncLogQuery }) - t.Run("when query mode is 'Logs' and does not include type or subtype", func(t *testing.T) { + t.Run("when query mode is 'Logs Insights' and does not include type or subtype", func(t *testing.T) { origExecuteSyncLogQuery := executeSyncLogQuery syncCalled := false executeSyncLogQuery = func(ctx context.Context, e *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { diff --git a/pkg/tsdb/cloudwatch/mocks/logs.go b/pkg/tsdb/cloudwatch/mocks/logs.go index 7843fdaacd1..da8dd2cc47c 100644 --- a/pkg/tsdb/cloudwatch/mocks/logs.go +++ b/pkg/tsdb/cloudwatch/mocks/logs.go @@ -66,3 +66,9 @@ func (m *MockLogEvents) GetLogEvents(ctx context.Context, input *cloudwatchlogs. return args.Get(0).(*cloudwatchlogs.GetLogEventsOutput), args.Error(1) } + +func (m *MockLogEvents) ListAnomalies(ctx context.Context, input *cloudwatchlogs.ListAnomaliesInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListAnomaliesOutput, error) { + args := m.Called(ctx, input, optFns) + + return args.Get(0).(*cloudwatchlogs.ListAnomaliesOutput), args.Error(1) +} diff --git a/pkg/tsdb/cloudwatch/models/api.go b/pkg/tsdb/cloudwatch/models/api.go index e4b487288f1..eed0243b69d 100644 --- a/pkg/tsdb/cloudwatch/models/api.go +++ b/pkg/tsdb/cloudwatch/models/api.go @@ -76,6 +76,7 @@ type CWLogsClient interface { cloudwatchlogs.GetLogEventsAPIClient cloudwatchlogs.DescribeLogGroupsAPIClient + cloudwatchlogs.ListAnomaliesAPIClient } type CWClient interface { diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index 82ce33099ee..3f4c797dea4 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -29,12 +29,15 @@ type fakeCWLogsClient struct { queryResults cloudwatchlogs.GetQueryResultsOutput logGroupsIndex int + + anomalies []cloudwatchlogstypes.Anomaly } type logsQueryCalls struct { startQuery []*cloudwatchlogs.StartQueryInput getEvents []*cloudwatchlogs.GetLogEventsInput describeLogGroups []*cloudwatchlogs.DescribeLogGroupsInput + listAnomalies []*cloudwatchlogs.ListAnomaliesInput } func (m *fakeCWLogsClient) GetQueryResults(_ context.Context, _ *cloudwatchlogs.GetQueryResultsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetQueryResultsOutput, error) { @@ -55,6 +58,14 @@ func (m *fakeCWLogsClient) StopQuery(_ context.Context, _ *cloudwatchlogs.StopQu }, nil } +func (m *fakeCWLogsClient) ListAnomalies(_ context.Context, input *cloudwatchlogs.ListAnomaliesInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListAnomaliesOutput, error) { + m.calls.listAnomalies = append(m.calls.listAnomalies, input) + + return &cloudwatchlogs.ListAnomaliesOutput{ + Anomalies: m.anomalies, + }, nil +} + type mockLogsSyncClient struct { mock.Mock } @@ -80,6 +91,11 @@ func (m *mockLogsSyncClient) StartQuery(ctx context.Context, input *cloudwatchlo return args.Get(0).(*cloudwatchlogs.StartQueryOutput), args.Error(1) } +func (m *mockLogsSyncClient) ListAnomalies(ctx context.Context, input *cloudwatchlogs.ListAnomaliesInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListAnomaliesOutput, error) { + args := m.Called(ctx, input, optFns) + return args.Get(0).(*cloudwatchlogs.ListAnomaliesOutput), args.Error(1) +} + func (m *fakeCWLogsClient) DescribeLogGroups(_ context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { m.calls.describeLogGroups = append(m.calls.describeLogGroups, input) output := &m.logGroups[m.logGroupsIndex] diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index b38b2c1cc07..08b951c353a 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -748,78 +748,191 @@ func processMetrics(esAgg *simplejson.Json, target *Query, query *backend.DataRe return nil } -func processAggregationDocs(esAgg *simplejson.Json, aggDef *BucketAgg, target *Query, - queryResult *backend.DataResponse, props map[string]string) error { - propKeys := createPropKeys(props) - frames := data.Frames{} - fields := createFields(queryResult.Frames, propKeys) - - for _, v := range esAgg.Get("buckets").MustArray() { - bucket := simplejson.NewFromAny(v) - var values []interface{} - - found := false - for _, field := range fields { - for _, propKey := range propKeys { - if field.Name == propKey { - value := props[propKey] - field.Append(&value) - } - } - if field.Name == aggDef.Field { - found = true - if key, err := bucket.Get("key").String(); err == nil { - field.Append(&key) - } else { - f, err := bucket.Get("key").Float64() - if err != nil { - return fmt.Errorf("error appending bucket key to existing field with name %s: %w", field.Name, err) - } - field.Append(&f) - } - } - } - - if !found { - var aggDefField *data.Field - if key, err := bucket.Get("key").String(); err == nil { - aggDefField = extractDataField(aggDef.Field, &key) - aggDefField.Append(&key) - } else { - f, err := bucket.Get("key").Float64() - if err != nil { - return fmt.Errorf("error appending bucket key to new field with name %s: %w", aggDef.Field, err) - } - aggDefField = extractDataField(aggDef.Field, &f) - aggDefField.Append(&f) - } - fields = append(fields, aggDefField) - } - - for _, metric := range target.Metrics { - switch metric.Type { - case countType: - addMetricValueToFields(&fields, values, getMetricName(metric.Type), castToFloat(bucket.Get("doc_count"))) - case extendedStatsType: - addExtendedStatsToFields(&fields, bucket, metric, values) - case percentilesType: - addPercentilesToFields(&fields, bucket, metric, values) - case topMetricsType: - addTopMetricsToFields(&fields, bucket, metric, values) - default: - addOtherMetricsToFields(&fields, bucket, metric, values, target) - } - } - - var dataFields []*data.Field - dataFields = append(dataFields, fields...) - - frames = data.Frames{ - &data.Frame{ - Fields: dataFields, - }} +// ensurePropFields guarantees all property columns exist even if prior frames lacked them +func ensurePropFields(fields *[]*data.Field, keys []string) { + have := map[string]bool{} + for _, f := range *fields { + have[f.Name] = true } - queryResult.Frames = frames + for _, k := range keys { + if !have[k] { + d := "" + f := extractDataField(k, &d) + *fields = append(*fields, f) + } + } +} + +// appendPropsRow appends one row of property values; skipKey avoids double-append +func appendPropsRow(fields *[]*data.Field, props map[string]string, propKeys []string, skipKey string) { + for _, f := range *fields { + for _, pk := range propKeys { + if pk == skipKey { + continue + } + if f.Name == pk { + val := props[pk] + f.Append(&val) + } + } + } +} + +// appendMetrics appends all metric values for a single bucket/row +func appendMetrics(fields *[]*data.Field, bucket *simplejson.Json, target *Query) { + var values []interface{} + for _, metric := range target.Metrics { + switch metric.Type { + case countType: + addMetricValueToFields(fields, values, getMetricName(metric.Type), castToFloat(bucket.Get("doc_count"))) + case extendedStatsType: + addExtendedStatsToFields(fields, bucket, metric, values) + case percentilesType: + addPercentilesToFields(fields, bucket, metric, values) + case topMetricsType: + addTopMetricsToFields(fields, bucket, metric, values) + default: + addOtherMetricsToFields(fields, bucket, metric, values, target) + } + } +} + +// appendKeyColumnString appends a string key to an existing field or creates it +func appendKeyColumnString(fields *[]*data.Field, fieldName, key string) { + for _, f := range *fields { + if f.Name == fieldName { + k := key + f.Append(&k) + return + } + } + k := key + f := extractDataField(fieldName, &k) + f.Append(&k) + *fields = append(*fields, f) +} + +// appendBucketKeyValue appends the bucket's "key" (string or number) to fieldName +func appendBucketKeyValue(fields *[]*data.Field, fieldName string, bucket *simplejson.Json) error { + for _, f := range *fields { + if f.Name == fieldName { + if s, err := bucket.Get("key").String(); err == nil { + f.Append(&s) + return nil + } + num, err := bucket.Get("key").Float64() + if err != nil { + return fmt.Errorf("error appending bucket key to existing field %q: %w", fieldName, err) + } + f.Append(&num) + return nil + } + } + + // field not present yet + if s, err := bucket.Get("key").String(); err == nil { + f := extractDataField(fieldName, &s) + f.Append(&s) + *fields = append(*fields, f) + return nil + } + + num, err := bucket.Get("key").Float64() + if err != nil { + return fmt.Errorf("error appending bucket key to new field %q: %w", fieldName, err) + } + + f := extractDataField(fieldName, &num) + f.Append(&num) + *fields = append(*fields, f) + + return nil +} + +func processAggregationDocs( + esAgg *simplejson.Json, + aggDef *BucketAgg, + target *Query, + queryResult *backend.DataResponse, + props map[string]string, +) error { + propKeys := createPropKeys(props) + buckets := esAgg.Get("buckets") + + if arr := buckets.MustArray(); len(arr) > 0 { + fields := createFields(queryResult.Frames, propKeys) + ensurePropFields(&fields, propKeys) + + for _, v := range arr { + bucket := simplejson.NewFromAny(v) + + appendPropsRow(&fields, props, propKeys, "") + if aggDef.Field != "" { + if err := appendBucketKeyValue(&fields, aggDef.Field, bucket); err != nil { + return err + } + } + appendMetrics(&fields, bucket, target) + } + + queryResult.Frames = data.Frames{&data.Frame{Fields: fields}} + return nil + } + + if m := buckets.MustMap(); len(m) > 0 { + // default key column to "filter" for leaf filters + keyFieldName := aggDef.Field + if keyFieldName == "" { + keyFieldName = "filter" + } + + // ensure "filter" exists among props + hasFilter := false + for _, pk := range propKeys { + if pk == "filter" { + hasFilter = true + break + } + } + if !hasFilter { + propKeys = append(propKeys, "filter") + } + + fields := createFields(queryResult.Frames, propKeys) + ensurePropFields(&fields, propKeys) + + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + bucket := simplejson.NewFromAny(m[k]) + + locProps := make(map[string]string, len(props)+1) + for kk, vv := range props { + locProps[kk] = vv + } + locProps["filter"] = k + + // avoid double-append when the key column is "filter" + skip := "" + if keyFieldName == "filter" { + skip = "filter" + } + + appendPropsRow(&fields, locProps, propKeys, skip) + appendKeyColumnString(&fields, keyFieldName, k) + appendMetrics(&fields, bucket, target) + } + + queryResult.Frames = data.Frames{&data.Frame{Fields: fields}} + return nil + } + + // no buckets present + queryResult.Frames = data.Frames{} return nil } @@ -1223,17 +1336,23 @@ func setLogsCustomMeta(frame *data.Frame, searchWords map[string]bool, limit int func createFields(frames data.Frames, propKeys []string) []*data.Field { var fields []*data.Field - // Otherwise use the fields from frames - if frames != nil { - for _, frame := range frames { - fields = append(fields, frame.Fields...) - } - // If we have no frames, we create fields from propKeys - } else { - for _, propKey := range propKeys { - fields = append(fields, data.NewField(propKey, nil, []*string{})) + have := map[string]bool{} + + // collect existing fields + for _, frame := range frames { + for _, f := range frame.Fields { + fields = append(fields, f) + have[f.Name] = true } } + + // add missing prop fields + for _, pk := range propKeys { + if !have[pk] { + fields = append(fields, data.NewField(pk, nil, []*string{})) + } + } + return fields } diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index 85239537bad..0032796cadf 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -3648,6 +3648,142 @@ func TestTrimEdges(t *testing.T) { requireFrameLength(t, frames[0], 1) } +func TestFiltersAggregation_KeyedBuckets(t *testing.T) { + t.Run("Leaf filters (keyed buckets) returns a table with filter | Count", func(t *testing.T) { + targets := map[string]string{ + "A": `{ + "metrics": [{ "type": "count", "id": "1" }], + "bucketAggs": [{ + "type": "filters", + "id": "2", + "settings": { + "filters": [ + { "label": "a 0-1 min", "query": "duration_seconds:[0 TO 60}" }, + { "label": "b 1-5 min", "query": "duration_seconds:[60 TO 300}" } + ] + } + }] + }`, + } + + // ES returns a keyed map for filters buckets (labels -> bucket) + response := `{ + "responses": [{ + "aggregations": { + "2": { + "buckets": { + "a 0-1 min": { "doc_count": 12 }, + "b 1-5 min": { "doc_count": 39 } + } + } + } + }] + }` + + result, err := parseTestResponse(targets, response, false) + require.NoError(t, err) + require.Len(t, result.Responses, 1) + + res := result.Responses["A"] + require.NotNil(t, res) + require.NoError(t, res.Error) + + frames := res.Frames + require.Len(t, frames, 1) + + frame := frames[0] + // expect exactly 2 rows (one per filter bucket) and 2 columns: filter | Count + requireFrameLength(t, frame, 2) + require.Len(t, frame.Fields, 2) + + // build field map for stable assertions + fieldMap := map[string]*data.Field{} + for _, f := range frame.Fields { + fieldMap[f.Name] = f + } + + require.Contains(t, fieldMap, "filter") + require.Contains(t, fieldMap, "Count") + + // keys are sorted lexicographically in the parser, so "a 0-1 min" then "b 1-5 min" + requireStringAt(t, "a 0-1 min", fieldMap["filter"], 0) + requireStringAt(t, "b 1-5 min", fieldMap["filter"], 1) + + requireFloatAt(t, 12, fieldMap["Count"], 0) + requireFloatAt(t, 39, fieldMap["Count"], 1) + }) + + t.Run("Filters -> Terms keeps the filter column and yields filter | type | Count", func(t *testing.T) { + targets := map[string]string{ + "A": `{ + "metrics": [{ "type": "count", "id": "1" }], + "bucketAggs": [ + { + "type": "filters", + "id": "2", + "settings": { + "filters": [ + { "label": "A", "query": "duration_seconds:[0 TO 60}" }, + { "label": "B", "query": "duration_seconds:[60 TO 300}" } + ] + } + }, + { "type": "terms", "field": "type", "id": "3" } + ] + }`, + } + + response := `{ + "responses": [{ + "aggregations": { + "2": { + "buckets": { + "A": { "3": { "buckets": [ { "key": "pull_request", "doc_count": 10 } ] } }, + "B": { "3": { "buckets": [ { "key": "pull_request", "doc_count": 5 } ] } } + } + } + } + }] + }` + + result, err := parseTestResponse(targets, response, false) + require.NoError(t, err) + require.Len(t, result.Responses, 1) + + res := result.Responses["A"] + require.NotNil(t, res) + require.NoError(t, res.Error) + + frames := res.Frames + require.Len(t, frames, 1) + + frame := frames[0] + // expect 2 rows (A, B) and 3 columns: filter | type | Count + requireFrameLength(t, frame, 2) + require.Len(t, frame.Fields, 3) + + fieldMap := map[string]*data.Field{} + for _, f := range frame.Fields { + fieldMap[f.Name] = f + } + + require.Contains(t, fieldMap, "filter") + require.Contains(t, fieldMap, "type") + require.Contains(t, fieldMap, "Count") + + // filters are sorted lexicographically: A, B + requireStringAt(t, "A", fieldMap["filter"], 0) + requireStringAt(t, "B", fieldMap["filter"], 1) + + // terms field "type" should repeat "pull_request" on each row + requireStringAt(t, "pull_request", fieldMap["type"], 0) + requireStringAt(t, "pull_request", fieldMap["type"], 1) + + requireFloatAt(t, 10, fieldMap["Count"], 0) + requireFloatAt(t, 5, fieldMap["Count"], 1) + }) +} + func parseTestResponse(tsdbQueries map[string]string, responseBody string, keepLabelsInResponse bool) (*backend.QueryDataResponse, error) { from := time.Date(2018, 5, 15, 17, 50, 0, 0, time.UTC) to := time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC) diff --git a/pkg/tsdb/grafana-pyroscope-datasource/instance.go b/pkg/tsdb/grafana-pyroscope-datasource/instance.go index e2fe25c94ef..244481aacbd 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/instance.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/instance.go @@ -3,7 +3,6 @@ package pyroscope import ( "context" "encoding/json" - "fmt" "net/http" "net/url" "slices" @@ -46,16 +45,13 @@ type PyroscopeDatasource struct { // NewPyroscopeDatasource creates a new datasource instance. func NewPyroscopeDatasource(ctx context.Context, httpClientProvider httpclient.Provider, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - ctxLogger := logger.FromContext(ctx) opt, err := settings.HTTPClientOptions(ctx) if err != nil { - ctxLogger.Error("Failed to get HTTP client options", "error", err, "function", logEntrypoint()) - return nil, err + return nil, backend.DownstreamErrorf("failed to get HTTP client options: %w. function: %s", err, logEntrypoint()) } httpClient, err := httpClientProvider.New(opt) if err != nil { - ctxLogger.Error("Failed to create HTTP client", "error", err, "function", logEntrypoint()) - return nil, err + return nil, backend.DownstreamErrorf("failed to create HTTP client: %w. function: %s", err, logEntrypoint()) } return &PyroscopeDatasource{ @@ -88,12 +84,9 @@ func (d *PyroscopeDatasource) CallResource(ctx context.Context, req *backend.Cal } func (d *PyroscopeDatasource) profileTypes(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - ctxLogger := logger.FromContext(ctx) - u, err := url.Parse(req.URL) if err != nil { - ctxLogger.Error("Failed to parse URL", "error", err, "function", logEntrypoint()) - return backend.DownstreamErrorf("URL could not be parsed: %w", err) + return backend.DownstreamErrorf("URL could not be parsed: %w. function: %s", err, logEntrypoint()) } query := u.Query() @@ -101,14 +94,12 @@ func (d *PyroscopeDatasource) profileTypes(ctx context.Context, req *backend.Cal if query.Has("start") && query.Has("end") { start, err = strconv.ParseInt(query.Get("start"), 10, 64) if err != nil { - ctxLogger.Error("Failed to parse start as int", "error", err, "function", logEntrypoint()) - return backend.DownstreamError(fmt.Errorf("failed to parse start as int: %w", err)) + return backend.DownstreamErrorf("failed to parse start as int: %w. function: %s", err, logEntrypoint()) } end, err = strconv.ParseInt(query.Get("end"), 10, 64) if err != nil { - ctxLogger.Error("Failed to parse end as int", "error", err, "function", logEntrypoint()) - return backend.DownstreamError(fmt.Errorf("failed to parse end as int: %w", err)) + return backend.DownstreamErrorf("failed to parse end as int: %w. function: %s", err, logEntrypoint()) } } else { // Make sure to pass a valid time range to the client as v2 will not work without it. @@ -118,29 +109,23 @@ func (d *PyroscopeDatasource) profileTypes(ctx context.Context, req *backend.Cal types, err := d.client.ProfileTypes(ctx, start, end) if err != nil { - ctxLogger.Error("Received error from client", "error", err, "function", logEntrypoint()) - return err + return backend.DownstreamErrorf("received error from client: %w. function: %s", err, logEntrypoint()) } bodyData, err := json.Marshal(types) if err != nil { - ctxLogger.Error("Failed to marshal response", "error", err, "function", logEntrypoint()) - return err + return backend.DownstreamErrorf("failed to marshal response: %w. function: %s", err, logEntrypoint()) } err = sender.Send(&backend.CallResourceResponse{Body: bodyData, Status: 200}) if err != nil { - ctxLogger.Error("Failed to send response", "error", err, "function", logEntrypoint()) - return err + return backend.DownstreamErrorf("failed to send response: %w. function: %s", err, logEntrypoint()) } return nil } func (d *PyroscopeDatasource) labelNames(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - ctxLogger := logger.FromContext(ctx) - u, err := url.Parse(req.URL) if err != nil { - ctxLogger.Error("Failed to parse URL", "error", err, "function", logEntrypoint()) - return backend.DownstreamError(fmt.Errorf("URL could not be parsed: %w", err)) + return backend.DownstreamErrorf("URL could not be parsed: %w. function: %s", err, logEntrypoint()) } query := u.Query() @@ -149,14 +134,12 @@ func (d *PyroscopeDatasource) labelNames(ctx context.Context, req *backend.CallR labelSelector := query.Get("query") matchers, err := parser.ParseMetricSelector(labelSelector) if err != nil { - ctxLogger.Error("Could not parse label selector", "error", err, "function", logEntrypoint()) - return backend.DownstreamError(fmt.Errorf("failed parsing label selector: %v", err)) + return backend.DownstreamErrorf("failed parsing label selector: %w. function: %s", err, logEntrypoint()) } labelNames, err := d.client.LabelNames(ctx, labelSelector, start, end) if err != nil { - ctxLogger.Error("Received error from client", "error", err, "function", logEntrypoint()) - return backend.DownstreamError(fmt.Errorf("error calling LabelNames: %v", err)) + return backend.DownstreamErrorf("error calling LabelNames: %w. function: %s", err, logEntrypoint()) } finalLabels := make([]string, 0) @@ -171,13 +154,11 @@ func (d *PyroscopeDatasource) labelNames(ctx context.Context, req *backend.CallR jsonResponse, err := json.Marshal(finalLabels) if err != nil { - ctxLogger.Error("Failed to marshal response", "error", err, "function", logEntrypoint()) - return err + return backend.DownstreamErrorf("failed to marshal response: %w. function: %s", err, logEntrypoint()) } err = sender.Send(&backend.CallResourceResponse{Body: jsonResponse, Status: 200}) if err != nil { - ctxLogger.Error("Failed to send response", "error", err, "function", logEntrypoint()) - return err + return backend.DownstreamErrorf("failed to send response: %w. function: %s", err, logEntrypoint()) } return nil } @@ -190,11 +171,9 @@ type LabelValuesPayload struct { } func (d *PyroscopeDatasource) labelValues(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - ctxLogger := logger.FromContext(ctx) u, err := url.Parse(req.URL) if err != nil { - ctxLogger.Error("Failed to parse URL", "error", err, "function", logEntrypoint()) - return backend.DownstreamError(fmt.Errorf("URL could not be parsed: %w", err)) + return backend.DownstreamErrorf("URL could not be parsed: %w. function: %s", err, logEntrypoint()) } query := u.Query() @@ -204,20 +183,17 @@ func (d *PyroscopeDatasource) labelValues(ctx context.Context, req *backend.Call res, err := d.client.LabelValues(ctx, label, query.Get("query"), start, end) if err != nil { - ctxLogger.Error("Received error from client", "error", err, "function", logEntrypoint()) - return backend.DownstreamError(fmt.Errorf("error calling LabelValues: %v", err)) + return backend.DownstreamErrorf("error calling LabelValues: %w. function: %s", err, logEntrypoint()) } data, err := json.Marshal(res) if err != nil { - ctxLogger.Error("Failed to marshal response", "error", err, "function", logEntrypoint()) - return backend.DownstreamErrorf("failed to marshall response: %w", err) + return backend.DownstreamErrorf("failed to marshall response: %w. function: %s", err, logEntrypoint()) } err = sender.Send(&backend.CallResourceResponse{Body: data, Status: 200}) if err != nil { - ctxLogger.Error("Failed to send response", "error", err, "function", logEntrypoint()) - return err + return backend.DownstreamErrorf("failed to send response: %w. function: %s", err, logEntrypoint()) } return nil @@ -227,13 +203,10 @@ func (d *PyroscopeDatasource) labelValues(ctx context.Context, req *backend.Call // for all known profile types, including their aggregation type (cumulative/instant), // units, descriptions, and grouping information. func (d *PyroscopeDatasource) profileMetadata(ctx context.Context, _ *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - ctxLogger := logger.FromContext(ctx) - registry := GetProfileMetadataRegistry() jsonData, err := json.Marshal(registry.profiles) if err != nil { - ctxLogger.Error("Failed to marshal profile metadata", "error", err, "function", logEntrypoint()) return sender.Send(&backend.CallResourceResponse{ Status: 500, Body: []byte(`{"error": "Failed to marshal profile metadata"}`), diff --git a/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go b/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go index da5d9309130..aa6af21d6c3 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go @@ -80,7 +80,6 @@ func (c *PyroscopeClient) ProfileTypes(ctx context.Context, start int64, end int End: end, })) if err != nil { - logger.Error("Received error from client", "error", err, "function", logEntrypoint()) span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return nil, backend.DownstreamError(fmt.Errorf("received error from client while getting profile types: %w", err)) @@ -115,10 +114,9 @@ func (c *PyroscopeClient) GetSeries(ctx context.Context, profileTypeID string, l resp, err := c.connectClient.SelectSeries(ctx, req) if err != nil { - logger.Error("Received error from client", "error", err, "function", logEntrypoint()) span.RecordError(err) span.SetStatus(codes.Error, err.Error()) - return nil, backend.DownstreamError(fmt.Errorf("received error from client while getting series: %w", err)) + return nil, backend.DownstreamErrorf("received error from client while getting series: %w", err) } series := make([]*Series, len(resp.Msg.Series)) @@ -171,7 +169,6 @@ func (c *PyroscopeClient) GetProfile(ctx context.Context, profileTypeID, labelSe resp, err := c.connectClient.SelectMergeStacktraces(ctx, req) if err != nil { - logger.Error("Received error from client", "error", err, "function", logEntrypoint()) span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return nil, backend.DownstreamError(fmt.Errorf("received error from client while getting profile: %w", err)) @@ -207,7 +204,7 @@ func (c *PyroscopeClient) GetSpanProfile(ctx context.Context, profileTypeID, lab } if resp.Msg.Flamegraph == nil { - // Not an error, can happen when querying data oout of range. + // Not an error, can happen when querying data out of range. return nil, nil } @@ -254,7 +251,6 @@ func (c *PyroscopeClient) LabelNames(ctx context.Context, labelSelector string, End: end, })) if err != nil { - logger.Error("Received error from client", "error", err, "function", logEntrypoint()) span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return nil, backend.DownstreamError(fmt.Errorf("error sending LabelNames request %v", err)) @@ -284,7 +280,6 @@ func (c *PyroscopeClient) LabelValues(ctx context.Context, label string, labelSe End: end, })) if err != nil { - logger.Error("Received error from client", "error", err, "function", logEntrypoint()) span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return nil, backend.DownstreamError(fmt.Errorf("received error from client while getting label values: %w", err)) diff --git a/pkg/tsdb/grafana-pyroscope-datasource/service.go b/pkg/tsdb/grafana-pyroscope-datasource/service.go index bad52d48984..04a4c5240fc 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/service.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/service.go @@ -87,10 +87,10 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) response, err := i.QueryData(ctx, req) if err != nil { - ctxLogger.Error("Received error from Pyroscope", "error", err, "function", logEntrypoint()) - } else { - ctxLogger.Debug("All queries processed", "function", logEntrypoint()) + return nil, backend.DownstreamErrorf("received error from Pyroscope while querying data: %s", err) } + + ctxLogger.Debug("All queries processed", "function", logEntrypoint()) return response, err } @@ -105,10 +105,10 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq err = i.CallResource(ctx, req, sender) if err != nil { - loggerWithContext.Error("Received error from Pyroscope", "error", err, "function", logEntrypoint()) - } else { - loggerWithContext.Debug("Health check succeeded", "function", logEntrypoint()) + return backend.DownstreamErrorf("received error from Pyroscope while calling resource: %w", err) } + + loggerWithContext.Debug("Health check succeeded", "function", logEntrypoint()) return err } @@ -123,10 +123,10 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque response, err := i.CheckHealth(ctx, req) if err != nil { - loggerWithContext.Error("Received error from Pyroscope", "error", err, "function", logEntrypoint()) - } else { - loggerWithContext.Debug("Health check succeeded", "function", logEntrypoint()) + return nil, backend.DownstreamErrorf("received error from Pyroscope while health checking: %w", err) } + + loggerWithContext.Debug("Health check succeeded", "function", logEntrypoint()) return response, err } @@ -141,10 +141,10 @@ func (s *Service) SubscribeStream(ctx context.Context, req *backend.SubscribeStr response, err := i.SubscribeStream(ctx, req) if err != nil { - loggerWithContext.Error("Received error from Pyroscope", "error", err, "function", logEntrypoint()) - } else { - loggerWithContext.Debug("Stream subscribed", "function", logEntrypoint()) + return nil, backend.DownstreamErrorf("received error from Pyroscope while subscribing the stream: %w", err) } + + loggerWithContext.Debug("Stream subscribed", "function", logEntrypoint()) return response, err } @@ -159,10 +159,10 @@ func (s *Service) RunStream(ctx context.Context, req *backend.RunStreamRequest, err = i.RunStream(ctx, req, sender) if err != nil { - loggerWithContext.Error("Received error from Pyroscope", "error", err, "function", logEntrypoint()) - } else { - loggerWithContext.Debug("Stream run", "function", logEntrypoint()) + return backend.DownstreamErrorf("received error from Pyroscope while trying to run the stream: %w", err) } + + loggerWithContext.Debug("Stream run", "function", logEntrypoint()) return err } @@ -178,9 +178,9 @@ func (s *Service) PublishStream(ctx context.Context, req *backend.PublishStreamR response, err := i.PublishStream(ctx, req) if err != nil { - loggerWithContext.Error("Received error from Pyroscope", "error", err, "function", logEntrypoint()) - } else { - loggerWithContext.Debug("Stream published", "function", logEntrypoint()) + return nil, backend.DownstreamErrorf("received error from Pyroscope while publishing the stream: %w", err) } + + loggerWithContext.Debug("Stream published", "function", logEntrypoint()) return response, err } diff --git a/pkg/tsdb/grafanads/grafana.go b/pkg/tsdb/grafanads/grafana.go index 3953e17dc5e..06254b7f0b8 100644 --- a/pkg/tsdb/grafanads/grafana.go +++ b/pkg/tsdb/grafanads/grafana.go @@ -169,12 +169,24 @@ func (s *Service) doReadQuery(ctx context.Context, query backend.DataQuery) back func (s *Service) doRandomWalk(query backend.DataQuery) backend.DataResponse { response := backend.DataResponse{} - model, err := testdatasource.GetJSONModel(json.RawMessage{}) + model, err := testdatasource.GetJSONModel(query.JSON) if err != nil { response.Error = err return response } - response.Frames = data.Frames{testdatasource.RandomWalk(query, model, 0)} + + // Default to 1 series if not specified + seriesCount := model.SeriesCount + if seriesCount == 0 { + seriesCount = 1 + } + + // Generate the requested number of series + frames := make([]*data.Frame, 0, seriesCount) + for i := 0; i < seriesCount; i++ { + frames = append(frames, testdatasource.RandomWalk(query, model, i)) + } + response.Frames = frames return response } diff --git a/pkg/tsdb/graphite/query.go b/pkg/tsdb/graphite/query.go index 6f4d2bca083..2ab60b21592 100644 --- a/pkg/tsdb/graphite/query.go +++ b/pkg/tsdb/graphite/query.go @@ -256,8 +256,14 @@ func (s *Service) parseResponse(res *http.Response) ([]TargetResponseDTO, error) var data []TargetResponseDTO err = json.Unmarshal(body, &data) if err != nil { - s.logger.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body)) - return nil, backend.DownstreamError(err) + s.logger.Warn("Failed to unamrshal to newer graphite response, attempting legacy") + var legacyData LegacyTargetResponseDTO + err = json.Unmarshal(body, &legacyData) + if err != nil { + s.logger.Info("Failed to unmarshal legacy graphite response", "error", err, "status", res.Status, "body", string(body)) + return nil, backend.PluginError(err) + } + return legacyData.Series, nil } return data, nil diff --git a/pkg/tsdb/graphite/query_test.go b/pkg/tsdb/graphite/query_test.go index 8af55a156a5..036f5401194 100644 --- a/pkg/tsdb/graphite/query_test.go +++ b/pkg/tsdb/graphite/query_test.go @@ -225,6 +225,70 @@ func TestConvertResponses(t *testing.T) { t.Errorf("Data frames should have been equal but was, expected:\n%s\nactual:\n%s", expectedFramesJSON, dataFramesJSON) } }) + + t.Run("Converts legacy response with no series", func(*testing.T) { + body := ` + { + "version": "v0.1", + "meta": { + "stats": {} + }, + "series": [] + }` + refId := "A" + expectedFrames := data.Frames{} + + httpResponse := &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body))} + dataFrames, err := service.toDataFrames(httpResponse, refId) + + require.NoError(t, err) + if !reflect.DeepEqual(expectedFrames, dataFrames) { + expectedFramesJSON, _ := json.Marshal(expectedFrames) + dataFramesJSON, _ := json.Marshal(dataFrames) + t.Errorf("Data frames should have been equal but was, expected:\n%s\nactual:\n%s", expectedFramesJSON, dataFramesJSON) + } + }) + + t.Run("Converts legacy response with series", func(*testing.T) { + body := ` + { + "version": "v0.1", + "meta": { + "stats": { + } + }, + "series": [ + { + "target": "target", + "tags": { "fooTag": "fooValue", "barTag": "barValue", "int": 100, "float": 3.14 }, + "datapoints": [[50, 1], [null, 2], [100, 3]] + } + ] + }` + refId := "A" + a := 50.0 + b := 100.0 + expectedFrame := data.NewFrame("A", + data.NewField("time", nil, []time.Time{time.Unix(1, 0).UTC(), time.Unix(2, 0).UTC(), time.Unix(3, 0).UTC()}), + data.NewField("value", data.Labels{ + "fooTag": "fooValue", + "barTag": "barValue", + "int": "100", + "float": "3.14", + }, []*float64{&a, nil, &b}).SetConfig(&data.FieldConfig{DisplayNameFromDS: "target"}), + ).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}) + expectedFrames := data.Frames{expectedFrame} + + httpResponse := &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body))} + dataFrames, err := service.toDataFrames(httpResponse, refId) + + require.NoError(t, err) + if !reflect.DeepEqual(expectedFrames, dataFrames) { + expectedFramesJSON, _ := json.Marshal(expectedFrames) + dataFramesJSON, _ := json.Marshal(dataFrames) + t.Errorf("Data frames should have been equal but was, expected:\n%s\nactual:\n%s", expectedFramesJSON, dataFramesJSON) + } + }) } func TestFixIntervalFormat(t *testing.T) { diff --git a/pkg/tsdb/graphite/types.go b/pkg/tsdb/graphite/types.go index 11c184b13de..7f836961b54 100644 --- a/pkg/tsdb/graphite/types.go +++ b/pkg/tsdb/graphite/types.go @@ -9,6 +9,12 @@ type TargetResponseDTO struct { Tags map[string]any `json:"tags"` } +type LegacyTargetResponseDTO struct { + Version string `json:"version"` + Meta map[string]any `json:"meta"` + Series []TargetResponseDTO `json:"series"` +} + type DataTimePoint [2]Float type DataTimeSeriesPoints []DataTimePoint diff --git a/pkg/tsdb/influxdb/fsql/arrow.go b/pkg/tsdb/influxdb/fsql/arrow.go index 832074576e6..9e8c3ffd08d 100644 --- a/pkg/tsdb/influxdb/fsql/arrow.go +++ b/pkg/tsdb/influxdb/fsql/arrow.go @@ -26,9 +26,7 @@ const rowLimit = 1_000_000 type recordReader interface { Next() bool Schema() *arrow.Schema - //TODO: fix deprecation warning - //nolint:staticcheck - Record() arrow.Record + Record() arrow.RecordBatch Err() error } diff --git a/pkg/tsdb/influxdb/fsql/arrow_test.go b/pkg/tsdb/influxdb/fsql/arrow_test.go index 6200e6a29ea..dec0fa4044b 100644 --- a/pkg/tsdb/influxdb/fsql/arrow_test.go +++ b/pkg/tsdb/influxdb/fsql/arrow_test.go @@ -77,12 +77,8 @@ func TestNewQueryDataResponse(t *testing.T) { arr = append(arr, tarr) } - //TODO: fix deprecation warning - //nolint:staticcheck - record := array.NewRecord(schema, arr, -1) - //TODO: fix deprecation warning - //nolint:staticcheck - records := []arrow.Record{record} + record := array.NewRecordBatch(schema, arr, -1) + records := []arrow.RecordBatch{record} reader, err := array.NewRecordReader(schema, records) assert.NoError(t, err) @@ -206,12 +202,8 @@ func TestNewQueryDataResponse_Error(t *testing.T) { ) assert.NoError(t, err) - //TODO: fix deprecation warning - //nolint:staticcheck - record := array.NewRecord(schema, []arrow.Array{i64s, f64s}, -1) - //TODO: fix deprecation warning - //nolint:staticcheck - records := []arrow.Record{record} + record := array.NewRecordBatch(schema, []arrow.Array{i64s, f64s}, -1) + records := []arrow.RecordBatch{record} reader, err := array.NewRecordReader(schema, records) assert.NoError(t, err) @@ -255,12 +247,8 @@ func TestNewQueryDataResponse_WideTable(t *testing.T) { ) assert.NoError(t, err) - //TODO: fix deprecation warning - //nolint:staticcheck - record := array.NewRecord(schema, []arrow.Array{times, strs, i64s}, -1) - //TODO: fix deprecation warning - //nolint:staticcheck - records := []arrow.Record{record} + record := array.NewRecordBatch(schema, []arrow.Array{times, strs, i64s}, -1) + records := []arrow.RecordBatch{record} reader, err := array.NewRecordReader(schema, records) assert.NoError(t, err) @@ -534,12 +522,8 @@ func TestCustomMetadata(t *testing.T) { ) assert.NoError(t, err) - //TODO: fix deprecation warning - //nolint:staticcheck - record := array.NewRecord(schema, []arrow.Array{i64s}, -1) - //TODO: fix deprecation warning - //nolint:staticcheck - records := []arrow.Record{record} + record := array.NewRecordBatch(schema, []arrow.Array{i64s}, -1) + records := []arrow.RecordBatch{record} reader, err := array.NewRecordReader(schema, records) assert.NoError(t, err) diff --git a/pkg/tsdb/jaeger/callresource.go b/pkg/tsdb/jaeger/callresource.go index 0d83be284d8..9b9cd2aa746 100644 --- a/pkg/tsdb/jaeger/callresource.go +++ b/pkg/tsdb/jaeger/callresource.go @@ -31,15 +31,29 @@ func (s *Service) withDatasourceHandlerFunc(getHandler func(d *datasourceInfo) h func getServicesHandler(ds *datasourceInfo) http.HandlerFunc { return func(rw http.ResponseWriter, r *http.Request) { - services, err := ds.JaegerClient.Services() + cfg := backend.GrafanaConfigFromContext(r.Context()) + var services []string + var err error + if cfg.FeatureToggles().IsEnabled("jaegerEnableGrpcEndpoint") { + services, err = ds.JaegerClient.GrpcServices() + } else { + services, err = ds.JaegerClient.Services() + } writeResponse(services, err, rw, ds.JaegerClient.logger) } } func getOperationsHandler(ds *datasourceInfo) http.HandlerFunc { return func(rw http.ResponseWriter, r *http.Request) { + cfg := backend.GrafanaConfigFromContext(r.Context()) service := strings.TrimSpace(r.PathValue("service")) - operations, err := ds.JaegerClient.Operations(service) + var operations []string + var err error + if cfg.FeatureToggles().IsEnabled("jaegerEnableGrpcEndpoint") { + operations, err = ds.JaegerClient.GrpcOperations(service) + } else { + operations, err = ds.JaegerClient.Operations(service) + } writeResponse(operations, err, rw, ds.JaegerClient.logger) } } diff --git a/pkg/tsdb/jaeger/client.go b/pkg/tsdb/jaeger/client.go index d858aa48f56..b2fe40e5eca 100644 --- a/pkg/tsdb/jaeger/client.go +++ b/pkg/tsdb/jaeger/client.go @@ -11,6 +11,9 @@ import ( "github.com/go-logfmt/logfmt" "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" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" + "github.com/grafana/grafana/pkg/tsdb/jaeger/utils" ) type JaegerClient struct { @@ -20,34 +23,6 @@ type JaegerClient struct { settings backend.DataSourceInstanceSettings } -type ServicesResponse struct { - Data []string `json:"data"` - Errors interface{} `json:"errors"` - Limit int `json:"limit"` - Offset int `json:"offset"` - Total int `json:"total"` -} - -type SettingsJSONData struct { - TraceIdTimeParams struct { - Enabled bool `json:"enabled"` - } `json:"traceIdTimeParams"` -} - -type DependenciesResponse struct { - Data []ServiceDependency `json:"data"` - Errors []struct { - Code int `json:"code"` - Msg string `json:"msg"` - } `json:"errors"` -} - -type ServiceDependency struct { - Parent string `json:"parent"` - Child string `json:"child"` - CallCount int `json:"callCount"` -} - func New(hc *http.Client, logger log.Logger, settings backend.DataSourceInstanceSettings) (JaegerClient, error) { client := JaegerClient{ logger: logger, @@ -59,12 +34,12 @@ func New(hc *http.Client, logger log.Logger, settings backend.DataSourceInstance } func (j *JaegerClient) Services() ([]string, error) { - var response ServicesResponse + var response types.ServicesResponse services := []string{} u, err := url.JoinPath(j.url, "/api/services") if err != nil { - return services, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err)) + return services, backend.DownstreamErrorf("failed to join url: %w", err) } res, err := j.httpClient.Get(u) @@ -87,12 +62,12 @@ func (j *JaegerClient) Services() ([]string, error) { } func (j *JaegerClient) Operations(s string) ([]string, error) { - var response ServicesResponse + var response types.ServicesResponse operations := []string{} u, err := url.JoinPath(j.url, "/api/services/", s, "/operations") if err != nil { - return operations, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err)) + return operations, backend.DownstreamErrorf("failed to join url: %w", err) } res, err := j.httpClient.Get(u) @@ -114,15 +89,15 @@ func (j *JaegerClient) Operations(s string) ([]string, error) { return operations, err } -func (j *JaegerClient) Search(query *JaegerQuery, start, end int64) ([]TraceResponse, error) { +func (j *JaegerClient) Search(query *JaegerQuery, start, end int64) (*data.Frame, error) { u, err := url.JoinPath(j.url, "/api/traces") if err != nil { - return []TraceResponse{}, backend.DownstreamError(fmt.Errorf("failed to join url path: %w", err)) + return nil, backend.DownstreamErrorf("failed to join url path: %w", err) } jaegerURL, err := url.Parse(u) if err != nil { - return []TraceResponse{}, backend.DownstreamError(fmt.Errorf("failed to parse Jaeger URL: %w", err)) + return nil, backend.DownstreamErrorf("failed to parse Jaeger URL: %w", err) } var queryTags string @@ -139,7 +114,7 @@ func (j *JaegerClient) Search(query *JaegerQuery, start, end int64) ([]TraceResp marshaledTags, err := json.Marshal(tagMap) if err != nil { - return []TraceResponse{}, backend.DownstreamError(fmt.Errorf("failed to convert tags to JSON: %w", err)) + return nil, backend.DownstreamErrorf("failed to convert tags to JSON: %w", err) } queryTags = string(marshaledTags) @@ -175,9 +150,9 @@ func (j *JaegerClient) Search(query *JaegerQuery, start, end int64) ([]TraceResp resp, err := j.httpClient.Get(jaegerURL.String()) if err != nil { if backend.IsDownstreamHTTPError(err) { - return []TraceResponse{}, backend.DownstreamError(err) + return nil, backend.DownstreamError(err) } - return []TraceResponse{}, err + return nil, err } defer func() { @@ -187,38 +162,38 @@ func (j *JaegerClient) Search(query *JaegerQuery, start, end int64) ([]TraceResp }() if resp.StatusCode != http.StatusOK { - err := backend.DownstreamError(fmt.Errorf("request failed: %s", resp.Status)) + err := fmt.Errorf("request failed: %s", resp.Status) if backend.ErrorSourceFromHTTPStatus(resp.StatusCode) == backend.ErrorSourceDownstream { - return []TraceResponse{}, backend.DownstreamError(err) + return nil, backend.DownstreamError(err) } - return []TraceResponse{}, err + return nil, err } - var result TracesResponse + var result types.TracesResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return []TraceResponse{}, fmt.Errorf("failed to decode Jaeger response: %w", err) + return nil, backend.DownstreamErrorf("failed to decode Jaeger response: %w", err) } - return result.Data, nil + frames := utils.TransformSearchResponse(result.Data, j.settings.UID, j.settings.Name) + return frames, nil } -func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int64) (TraceResponse, error) { +func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int64, refID string) (*data.Frame, error) { logger := j.logger.FromContext(ctx) - var response TracesResponse - trace := TraceResponse{} + var response types.TracesResponse if traceID == "" { - return trace, backend.DownstreamError(fmt.Errorf("traceID is empty")) + return nil, backend.DownstreamErrorf("traceID is empty") } traceUrl, err := url.JoinPath(j.url, "/api/traces", url.QueryEscape(traceID)) if err != nil { - return trace, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err)) + return nil, backend.DownstreamErrorf("failed to join url path: %w", err) } - var jsonData SettingsJSONData + var jsonData types.SettingsJSONData if err := json.Unmarshal(j.settings.JSONData, &jsonData); err != nil { - return trace, backend.DownstreamError(fmt.Errorf("failed to parse settings JSON data: %w", err)) + return nil, backend.DownstreamErrorf("failed to parse settings JSON data: %w", err) } // Add time parameters if trace ID time is enabled and time range is provided @@ -226,7 +201,7 @@ func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int if start > 0 || end > 0 { parsedURL, err := url.Parse(traceUrl) if err != nil { - return trace, backend.DownstreamError(fmt.Errorf("failed to parse url: %w", err)) + return nil, backend.DownstreamErrorf("failed to parse url: %w", err) } query := parsedURL.Query() @@ -245,9 +220,9 @@ func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int res, err := j.httpClient.Get(traceUrl) if err != nil { if backend.IsDownstreamHTTPError(err) { - return trace, backend.DownstreamError(err) + return nil, backend.DownstreamError(err) } - return trace, err + return nil, err } defer func() { @@ -257,36 +232,36 @@ func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int }() if res != nil && res.StatusCode/100 != 2 { - err := backend.DownstreamError(fmt.Errorf("request failed: %s", res.Status)) + err := fmt.Errorf("request failed: %s", res.Status) if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { - return trace, backend.DownstreamError(err) + return nil, backend.DownstreamError(err) } - return trace, err + return nil, err } if err := json.NewDecoder(res.Body).Decode(&response); err != nil { - return trace, err + return nil, err } // We only support one trace at a time // this is how it was implemented in the frontend before - trace = response.Data[0] - return trace, err + frames := utils.TransformTraceResponse(response.Data[0], refID) + return frames, err } -func (j *JaegerClient) Dependencies(ctx context.Context, start, end int64) (DependenciesResponse, error) { +func (j *JaegerClient) Dependencies(ctx context.Context, start, end int64) (types.DependenciesResponse, error) { logger := j.logger.FromContext(ctx) - var dependencies DependenciesResponse + var dependencies types.DependenciesResponse u, err := url.JoinPath(j.url, "/api/dependencies") if err != nil { - return dependencies, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err)) + return dependencies, backend.DownstreamErrorf("failed to join url path: %w", err) } // Add time parameters parsedURL, err := url.Parse(u) if err != nil { - return dependencies, backend.DownstreamError(fmt.Errorf("failed to parse url: %w", err)) + return dependencies, backend.DownstreamErrorf("failed to parse url: %w", err) } query := parsedURL.Query() @@ -316,7 +291,7 @@ func (j *JaegerClient) Dependencies(ctx context.Context, start, end int64) (Depe }() if res != nil && res.StatusCode/100 != 2 { - err := backend.DownstreamError(fmt.Errorf("request failed: %s", res.Status)) + err := fmt.Errorf("request failed: %s", res.Status) if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { return dependencies, backend.DownstreamError(err) } diff --git a/pkg/tsdb/jaeger/client_test.go b/pkg/tsdb/jaeger/client_test.go index eceb103ab82..54d3091f07e 100644 --- a/pkg/tsdb/jaeger/client_test.go +++ b/pkg/tsdb/jaeger/client_test.go @@ -379,7 +379,7 @@ func TestJaegerClient_Trace(t *testing.T) { client, err := New(server.Client(), log.NewNullLogger(), settings) assert.NoError(t, err) - trace, err := client.Trace(context.Background(), tt.traceId, tt.start, tt.end) + trace, err := client.Trace(context.Background(), tt.traceId, tt.start, tt.end, "A") if tt.expectError { assert.Error(t, err) diff --git a/pkg/tsdb/jaeger/grpc_client.go b/pkg/tsdb/jaeger/grpc_client.go new file mode 100644 index 00000000000..ec3e7214aa4 --- /dev/null +++ b/pkg/tsdb/jaeger/grpc_client.go @@ -0,0 +1,272 @@ +package jaeger + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/go-logfmt/logfmt" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" + "github.com/grafana/grafana/pkg/tsdb/jaeger/utils" +) + +func (j *JaegerClient) GrpcServices() ([]string, error) { + var response types.GrpcServicesResponse + services := []string{} + + u, err := url.JoinPath(j.url, "/api/v3/services") + if err != nil { + return services, backend.DownstreamErrorf("failed to join url: %w", err) + } + + res, err := j.httpClient.Get(u) + if err != nil { + if backend.IsDownstreamHTTPError(err) { + return services, backend.DownstreamError(err) + } + return services, err + } + + defer func() { + if err = res.Body.Close(); err != nil { + j.logger.Error("Failed to close response body", "error", err) + } + }() + + if res != nil && res.StatusCode != http.StatusOK { + err := fmt.Errorf("request failed: %s", res.Status) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return services, backend.DownstreamError(err) + } + return services, err + } + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return services, backend.DownstreamError(err) + } + + services = response.Services + return services, nil +} + +func (j *JaegerClient) GrpcOperations(s string) ([]string, error) { + var response types.GrpcOperationsResponse + operations := []string{} + + u, err := url.JoinPath(j.url, "/api/v3/operations") + if err != nil { + return operations, backend.DownstreamErrorf("failed to join url: %w", err) + } + + jaegerURL, err := url.Parse(u) + if err != nil { + return operations, backend.DownstreamErrorf("failed to parse Jaeger URL: %w", err) + } + + urlQuery := jaegerURL.Query() + urlQuery.Set("service", s) + jaegerURL.RawQuery = urlQuery.Encode() + + res, err := j.httpClient.Get(jaegerURL.String()) + if err != nil { + if backend.IsDownstreamHTTPError(err) { + return operations, backend.DownstreamError(err) + } + return operations, err + } + + defer func() { + if err = res.Body.Close(); err != nil { + j.logger.Error("Failed to close response body", "error", err) + } + }() + + if res != nil && res.StatusCode != http.StatusOK { + err := fmt.Errorf("request failed: %s", res.Status) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return operations, backend.DownstreamError(err) + } + return operations, err + } + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return operations, backend.DownstreamError(err) + } + + // extract name from operations response + for _, op := range response.Operations { + operations = append(operations, op.Name) + } + + return operations, nil +} + +// Note that this and all functionality around search is not yet being used. Once Jaeger adds support for attributes and limit parameters +// we will be able to start using this and routing traffic to the new API based on the feature flag. +func (j *JaegerClient) GrpcSearch(query *JaegerQuery, start, end time.Time) (*data.Frame, error) { + u, err := url.JoinPath(j.url, "/api/v3/traces") + if err != nil { + return nil, backend.DownstreamErrorf("failed to join url path: %w", err) + } + + jaegerURL, err := url.Parse(u) + if err != nil { + return nil, backend.DownstreamErrorf("failed to parse Jaeger URL: %w", err) + } + + var queryTags string + if query.Tags != "" { + tagMap := make(map[string]string) + decoder := logfmt.NewDecoder(strings.NewReader(query.Tags)) + for decoder.ScanRecord() { + for decoder.ScanKeyval() { + key := decoder.Key() + value := decoder.Value() + tagMap[string(key)] = string(value) + } + } + + marshaledTags, err := json.Marshal(tagMap) + if err != nil { + return nil, backend.DownstreamErrorf("failed to convert tags to JSON: %w", err) + } + + queryTags = string(marshaledTags) + } + + queryParams := map[string]string{ + "query.service_name": query.Service, + "query.operation_name": query.Operation, + "query.attributes": queryTags, // TODO: no native support of attributes/tags figure out if we want to do it in post processing. + "query.duration_min": query.MinDuration, + "query.duration_max": query.MaxDuration, + } + + urlQuery := jaegerURL.Query() + + for key, value := range queryParams { + if value != "" { + urlQuery.Set(key, value) + } + } + + jaegerURL.RawQuery = urlQuery.Encode() + // jaeger will not be able to process the request if the time is encoded, all other parameters are encoded except for the start and end time + jaegerURL.RawQuery += fmt.Sprintf("&query.start_time_min=%s&query.start_time_max=%s", start.Format(time.RFC3339Nano), end.Format(time.RFC3339Nano)) + resp, err := j.httpClient.Get(jaegerURL.String()) + if err != nil { + if backend.IsDownstreamHTTPError(err) { + return nil, backend.DownstreamError(err) + } + return nil, err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + j.logger.Error("Failed to close response body", "error", err) + } + }() + + if resp != nil && resp.StatusCode != http.StatusOK { + err := fmt.Errorf("request failed: %s", resp.Status) + if backend.ErrorSourceFromHTTPStatus(resp.StatusCode) == backend.ErrorSourceDownstream { + return nil, backend.DownstreamError(err) + } + return nil, err + } + + var response types.GrpcTracesResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, backend.DownstreamErrorf("failed to decode Jaeger response: %w", err) + } + + // for search call, an unsuccessful response is exposed through the error attribute + // see https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L77C17-L79C18 + if response.Error.HttpCode != 0 && response.Error.HttpCode != http.StatusOK { + err := fmt.Errorf("request failed %s", response.Error.Message) + if backend.ErrorSourceFromHTTPStatus(response.Error.HttpCode) == backend.ErrorSourceDownstream { + return nil, backend.DownstreamError(err) + } + return nil, err + } + frames := utils.TransformGrpcSearchResponse(response.Result, j.settings.UID, j.settings.Name, query.Limit) + return frames, nil +} + +func (j *JaegerClient) GrpcTrace(ctx context.Context, traceID string, start, end time.Time, refID string) (*data.Frame, error) { + logger := j.logger.FromContext(ctx) + var response types.GrpcTracesResponse + + if traceID == "" { + return nil, backend.DownstreamErrorf("traceID is empty") + } + + traceUrl, err := url.JoinPath(j.url, "/api/v3/traces", url.QueryEscape(traceID)) + if err != nil { + return nil, backend.DownstreamErrorf("failed to join url: %w", err) + } + + var jsonData types.SettingsJSONData + if err := json.Unmarshal(j.settings.JSONData, &jsonData); err != nil { + return nil, backend.DownstreamErrorf("failed to parse settings JSON data: %w", err) + } + + // Add time parameters if trace ID time is enabled and time range is provided + if jsonData.TraceIdTimeParams.Enabled { + if start.UnixMicro() > 0 || end.UnixMicro() > 0 { + parsedURL, err := url.Parse(traceUrl) + if err != nil { + return nil, backend.DownstreamErrorf("failed to parse url: %w", err) + } + + // jaeger will not be able to process the request if the time is encoded, all other parameters are encoded except for the start and end time + parsedURL.RawQuery += fmt.Sprintf("start_time=%s&end_time=%s", start.Format(time.RFC3339Nano), end.Format(time.RFC3339Nano)) + traceUrl = parsedURL.String() + } + } + + res, err := j.httpClient.Get(traceUrl) + if err != nil { + if backend.IsDownstreamHTTPError(err) { + return nil, backend.DownstreamError(err) + } + return nil, err + } + + defer func() { + if err = res.Body.Close(); err != nil { + logger.Error("Failed to close response body", "error", err) + } + }() + + if res != nil && res.StatusCode != http.StatusOK { + err := fmt.Errorf("request failed: %s", res.Status) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return nil, backend.DownstreamError(err) + } + return nil, err + } + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return nil, err + } + + // for trace search call, an unsuccessful response is exposed through the error attribute + // see https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L77C17-L79C18 + if response.Error.HttpCode != 0 && response.Error.HttpCode != http.StatusOK { + err := fmt.Errorf("request failed %s", response.Error.Message) + if backend.ErrorSourceFromHTTPStatus(response.Error.HttpCode) == backend.ErrorSourceDownstream { + return nil, backend.DownstreamError(err) + } + return nil, err + } + + frame := utils.TransformGrpcTraceResponse(response.Result.ResourceSpans, refID) + return frame, nil +} diff --git a/pkg/tsdb/jaeger/grpc_client_test.go b/pkg/tsdb/jaeger/grpc_client_test.go new file mode 100644 index 00000000000..c467284a7b5 --- /dev/null +++ b/pkg/tsdb/jaeger/grpc_client_test.go @@ -0,0 +1,403 @@ +package jaeger + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + "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" + "github.com/grafana/grafana-plugin-sdk-go/experimental/status" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJaegerGrpcClient_Services(t *testing.T) { + tests := []struct { + name string + mockResponse string + mockStatusCode int + mockStatus string + expectedResult []string + expectError bool + expectedError error + }{ + { + name: "Successful response", + mockResponse: `{"services": ["service1", "service2"]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{"service1", "service2"}, + expectError: false, + expectedError: nil, + }, + { + name: "Non-200 response", + mockResponse: "", + mockStatusCode: http.StatusInternalServerError, + mockStatus: "Internal Server Error", + expectedResult: []string{}, + expectError: true, + expectedError: backend.DownstreamError(errors.New("Internal Server Error")), + }, + { + name: "Invalid JSON response", + mockResponse: `{invalid json`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{}, + expectError: true, + expectedError: status.ErrorWithSource{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.mockStatusCode) + _, _ = w.Write([]byte(tt.mockResponse)) + })) + defer server.Close() + + settings := backend.DataSourceInstanceSettings{ + URL: server.URL, + } + client, err := New(server.Client(), log.NewNullLogger(), settings) + assert.NoError(t, err) + + services, err := client.GrpcServices() + + if tt.expectError { + assert.Error(t, err) + if tt.expectedError != nil { + assert.IsType(t, tt.expectedError, err) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, services) + } + }) + } +} + +func TestJaegerGrpcClient_Trace(t *testing.T) { + const successTraceResponse = `{ + "result": { + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "orders" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "exampleScope", + "version": "1.0.0" + }, + "spans": [ + { + "traceId": "abcd1234", + "spanId": "abcd5678", + "parentSpanId": "", + "name": "GET /ready", + "kind": 1, + "startTimeUnixNano": "1000", + "endTimeUnixNano": "2000", + "attributes": [], + "events": [], + "links": [], + "status": { + "message": "", + "code": 0 + } + } + ] + } + ], + "schemaUrl": "" + } + ] + }, + "error": { + "httpCode": 0, + "message": "", + "details": [] + } + }` + + t.Run("requires non-empty traceID", func(t *testing.T) { + settings := backend.DataSourceInstanceSettings{ + URL: "http://example.com", + JSONData: []byte(`{}`), + } + + client, err := New(http.DefaultClient, log.NewNullLogger(), settings) + require.NoError(t, err) + + frame, err := client.GrpcTrace(context.Background(), "", time.Time{}, time.Time{}, "A") + assert.Nil(t, frame) + assert.Error(t, err) + assert.IsType(t, status.ErrorWithSource{}, err) + assert.Contains(t, err.Error(), "traceID is empty") + }) + + t.Run("returns error when settings JSON is invalid", func(t *testing.T) { + settings := backend.DataSourceInstanceSettings{ + URL: "http://example.com", + JSONData: []byte(`{"traceIdTimeParams":`), + } + + client, err := New(http.DefaultClient, log.NewNullLogger(), settings) + require.NoError(t, err) + + frame, err := client.GrpcTrace(context.Background(), "trace-1", time.Time{}, time.Time{}, "A") + assert.Nil(t, frame) + assert.Error(t, err) + assert.IsType(t, status.ErrorWithSource{}, err) + assert.Contains(t, err.Error(), "failed to parse settings JSON data") + }) + + t.Run("propagates HTTP errors as downstream errors", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + settings := backend.DataSourceInstanceSettings{ + URL: server.URL, + JSONData: []byte(`{}`), + } + + client, err := New(server.Client(), log.NewNullLogger(), settings) + require.NoError(t, err) + + frame, err := client.GrpcTrace(context.Background(), "trace-1", time.Time{}, time.Time{}, "A") + assert.Nil(t, frame) + assert.Error(t, err) + assert.IsType(t, status.ErrorWithSource{}, err) + }) + + t.Run("returns error when response body is invalid", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{invalid`)) + })) + defer server.Close() + + settings := backend.DataSourceInstanceSettings{ + URL: server.URL, + JSONData: []byte(`{}`), + } + + client, err := New(server.Client(), log.NewNullLogger(), settings) + require.NoError(t, err) + + frame, err := client.GrpcTrace(context.Background(), "trace-1", time.Time{}, time.Time{}, "A") + assert.Nil(t, frame) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid character") + }) + + t.Run("returns error when Jaeger reports an error in payload", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "result": { + "resourceSpans": [] + }, + "error": { + "httpCode": 500, + "message": "upstream failure" + } + }`)) + })) + defer server.Close() + + settings := backend.DataSourceInstanceSettings{ + URL: server.URL, + JSONData: []byte(`{}`), + } + + client, err := New(server.Client(), log.NewNullLogger(), settings) + require.NoError(t, err) + + frame, err := client.GrpcTrace(context.Background(), "trace-1", time.Time{}, time.Time{}, "A") + assert.Nil(t, frame) + assert.Error(t, err) + assert.IsType(t, status.ErrorWithSource{}, err) + assert.Contains(t, err.Error(), "upstream failure") + }) + + t.Run("adds time range parameters when enabled", func(t *testing.T) { + start := time.Unix(1713276200, 0).UTC() + end := start.Add(3 * time.Second) + + var receivedQuery string + var receivedPath string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedQuery = r.URL.RawQuery + receivedPath = r.URL.Path + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(successTraceResponse)) + })) + defer server.Close() + + settings := backend.DataSourceInstanceSettings{ + URL: server.URL, + JSONData: []byte(`{"traceIdTimeParams":{"enabled":true}}`), + } + + client, err := New(server.Client(), log.NewNullLogger(), settings) + require.NoError(t, err) + + frame, err := client.GrpcTrace(context.Background(), "trace-1", start, end, "RefA") + assert.NoError(t, err) + assert.NotNil(t, frame) + + expectedStart := start.Format(time.RFC3339Nano) + expectedEnd := end.Format(time.RFC3339Nano) + assert.Equal(t, "/api/v3/traces/trace-1", receivedPath) + assert.Contains(t, receivedQuery, "start_time="+expectedStart) + assert.Contains(t, receivedQuery, "end_time="+expectedEnd) + }) + + t.Run("returns transformed frame on success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(successTraceResponse)) + })) + defer server.Close() + + settings := backend.DataSourceInstanceSettings{ + URL: server.URL, + JSONData: []byte(`{}`), + } + + client, err := New(server.Client(), log.NewNullLogger(), settings) + require.NoError(t, err) + + frame, err := client.GrpcTrace(context.Background(), "abcd1234", time.Time{}, time.Time{}, "RefA") + assert.NoError(t, err) + require.NotNil(t, frame) + assert.Equal(t, "RefA", frame.Name) + assert.Equal(t, 1, frame.Fields[0].Len()) + assert.NotNil(t, frame.Meta) + assert.Equal(t, string(data.VisTypeTrace), string(frame.Meta.PreferredVisualization)) + }) +} + +func TestJaegerGrpcClient_Operations(t *testing.T) { + tests := []struct { + name string + service string + mockResponse string + mockStatusCode int + mockStatus string + expectedResult []string + expectError bool + expectedError error + }{ + { + name: "Successful response", + service: "test-service", + mockResponse: `{"operations": [ + { + "name": "operation1", + "spanKind": "client" + }, + { + "name": "operation2", + "spanKind": "client" + } + ]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{"operation1", "operation2"}, + expectError: false, + expectedError: nil, + }, + { + name: "Non-200 response", + service: "test-service", + mockResponse: "", + mockStatusCode: http.StatusInternalServerError, + mockStatus: "Internal Server Error", + expectedResult: []string{}, + expectError: true, + expectedError: backend.DownstreamError(errors.New("Internal Server Error")), + }, + { + name: "Invalid JSON response", + service: "test-service", + mockResponse: `{invalid json`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{}, + expectError: true, + expectedError: status.ErrorWithSource{}, + }, + { + name: "Service with special characters", + service: "test/service:1", + mockResponse: `{"operations": [ + { + "name": "operation1" + } + ]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{"operation1"}, + expectError: false, + expectedError: nil, + }, + { + name: "Empty service", + service: "", + mockResponse: `{"operations": []}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{}, + expectError: false, + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.mockStatusCode) + _, _ = w.Write([]byte(tt.mockResponse)) + })) + defer server.Close() + + settings := backend.DataSourceInstanceSettings{ + URL: server.URL, + } + client, err := New(server.Client(), log.NewNullLogger(), settings) + assert.NoError(t, err) + + operations, err := client.GrpcOperations(tt.service) + + if tt.expectError { + assert.Error(t, err) + if tt.expectedError != nil { + assert.IsType(t, tt.expectedError, err) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, operations) + } + }) + } +} diff --git a/pkg/tsdb/jaeger/jaeger.go b/pkg/tsdb/jaeger/jaeger.go index a5c71c4327a..4855af4a91f 100644 --- a/pkg/tsdb/jaeger/jaeger.go +++ b/pkg/tsdb/jaeger/jaeger.go @@ -59,6 +59,10 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins logger := logger.FromContext(ctx) jaegerClient, err := New(httpClient, logger, settings) + if err != nil { + return nil, fmt.Errorf("error creating jaeger client: %w", err) + } + return &datasourceInfo{JaegerClient: jaegerClient}, err } } @@ -79,6 +83,7 @@ func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { client, err := s.getDSInfo(ctx, backend.PluginConfigFromContext(ctx)) + cfg := backend.GrafanaConfigFromContext(ctx) if err != nil { return &backend.CheckHealthResult{ Status: backend.HealthStatusError, @@ -86,10 +91,17 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque }, nil } - if _, err = client.JaegerClient.Services(); err != nil { + var servicesErr error + if cfg.FeatureToggles().IsEnabled("jaegerEnableGrpcEndpoint") { + _, servicesErr = client.JaegerClient.GrpcServices() + } else { + _, servicesErr = client.JaegerClient.Services() + } + + if servicesErr != nil { return &backend.CheckHealthResult{ Status: backend.HealthStatusError, - Message: err.Error(), + Message: servicesErr.Error(), }, nil } diff --git a/pkg/tsdb/jaeger/jaeger_test.go b/pkg/tsdb/jaeger/jaeger_test.go index 7dc09d3834f..d5ffac8ed96 100644 --- a/pkg/tsdb/jaeger/jaeger_test.go +++ b/pkg/tsdb/jaeger/jaeger_test.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -85,7 +86,7 @@ func TestDataSourceInstanceSettings_TraceIdTimeEnabled(t *testing.T) { // Verify the client's traceIdTimeEnabled parameter - var jsonData SettingsJSONData + var jsonData types.SettingsJSONData if err := json.Unmarshal(dsInfo.JaegerClient.settings.JSONData, &jsonData); err != nil { t.Fatalf("failed to parse settings JSON data: %v", err) } diff --git a/pkg/tsdb/jaeger/querydata.go b/pkg/tsdb/jaeger/querydata.go index ae34f695030..dcba098a7a8 100644 --- a/pkg/tsdb/jaeger/querydata.go +++ b/pkg/tsdb/jaeger/querydata.go @@ -5,10 +5,10 @@ import ( "encoding/json" "fmt" "sort" - "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" ) type JaegerQuery struct { @@ -46,14 +46,16 @@ func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDa continue } + cfg := backend.GrafanaConfigFromContext(ctx) + useGrpc := cfg.FeatureToggles().IsEnabled("jaegerEnableGrpcEndpoint") // Handle "Search" query type if query.QueryType == "search" { - traces, err := dsInfo.JaegerClient.Search(&query, q.TimeRange.From.UnixMicro(), q.TimeRange.To.UnixMicro()) + // TODO: enable routing to gRPC when ready, currently pending on: https://github.com/jaegertracing/jaeger/issues/7594 + frames, err := dsInfo.JaegerClient.Search(&query, q.TimeRange.From.UnixMicro(), q.TimeRange.To.UnixMicro()) if err != nil { response.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(err) continue } - frames := transformSearchResponse(traces, dsInfo) response.Responses[q.RefID] = backend.DataResponse{ Frames: data.Frames{frames}, } @@ -61,18 +63,24 @@ func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDa // No query type means traceID query if query.QueryType == "" { - traces, err := dsInfo.JaegerClient.Trace(ctx, query.Query, q.TimeRange.From.UnixMilli(), q.TimeRange.To.UnixMilli()) + var frame *data.Frame + var err error + if useGrpc { + frame, err = dsInfo.JaegerClient.GrpcTrace(ctx, query.Query, q.TimeRange.From, q.TimeRange.To, q.RefID) + } else { + frame, err = dsInfo.JaegerClient.Trace(ctx, query.Query, q.TimeRange.From.UnixMilli(), q.TimeRange.To.UnixMilli(), q.RefID) + } if err != nil { response.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(err) continue } - frame := transformTraceResponse(traces, q.RefID) response.Responses[q.RefID] = backend.DataResponse{ Frames: []*data.Frame{frame}, } } if query.QueryType == "dependencyGraph" { + // TODO: enable routing to gRPC when ready, currently pending on: https://github.com/jaegertracing/jaeger/issues/7595 dependencies, err := dsInfo.JaegerClient.Dependencies(ctx, q.TimeRange.From.UnixMilli(), q.TimeRange.To.UnixMilli()) if err != nil { response.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(err) @@ -93,209 +101,7 @@ func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDa return response, nil } -func transformSearchResponse(response []TraceResponse, dsInfo *datasourceInfo) *data.Frame { - // Create a frame for the traces - frame := data.NewFrame("traces", - data.NewField("traceID", nil, []string{}).SetConfig(&data.FieldConfig{ - DisplayName: "Trace ID", - Links: []data.DataLink{ - { - Title: "Trace: ${__value.raw}", - URL: "", - Internal: &data.InternalDataLink{ - DatasourceUID: dsInfo.JaegerClient.settings.UID, - DatasourceName: dsInfo.JaegerClient.settings.Name, - Query: map[string]interface{}{ - "query": "${__value.raw}", - }, - }, - }, - }, - }), - data.NewField("traceName", nil, []string{}).SetConfig(&data.FieldConfig{ - DisplayName: "Trace name", - }), - data.NewField("startTime", nil, []time.Time{}).SetConfig(&data.FieldConfig{ - DisplayName: "Start time", - }), - data.NewField("duration", nil, []int64{}).SetConfig(&data.FieldConfig{ - DisplayName: "Duration", - Unit: "µs", - }), - ) - - // Set the visualization type to table - frame.Meta = &data.FrameMeta{ - PreferredVisualization: "table", - } - - // Sort traces by start time in descending order (newest first) - sort.Slice(response, func(i, j int) bool { - rootSpanI := response[i].Spans[0] - rootSpanJ := response[j].Spans[0] - - for _, span := range response[i].Spans { - if span.StartTime < rootSpanI.StartTime { - rootSpanI = span - } - } - - for _, span := range response[j].Spans { - if span.StartTime < rootSpanJ.StartTime { - rootSpanJ = span - } - } - - return rootSpanI.StartTime > rootSpanJ.StartTime - }) - - // Process each trace - for _, trace := range response { - if len(trace.Spans) == 0 { - continue - } - - // Get the root span - rootSpan := trace.Spans[0] - for _, span := range trace.Spans { - if span.StartTime < rootSpan.StartTime { - rootSpan = span - } - } - - // Get the service name for the trace - serviceName := "" - if process, ok := trace.Processes[rootSpan.ProcessID]; ok { - serviceName = process.ServiceName - } - - // Get the trace name and start time - traceName := fmt.Sprintf("%s: %s", serviceName, rootSpan.OperationName) - startTime := time.Unix(0, rootSpan.StartTime*1000) - - // Append the row to the frame - frame.AppendRow( - trace.TraceID, - traceName, - startTime, - rootSpan.Duration, - ) - } - - return frame -} - -func transformTraceResponse(trace TraceResponse, refID string) *data.Frame { - frame := data.NewFrame(refID, - data.NewField("traceID", nil, []string{}), - data.NewField("spanID", nil, []string{}), - data.NewField("parentSpanID", nil, []*string{}), - data.NewField("operationName", nil, []string{}), - data.NewField("serviceName", nil, []string{}), - data.NewField("serviceTags", nil, []json.RawMessage{}), - data.NewField("startTime", nil, []float64{}), - data.NewField("duration", nil, []float64{}), - data.NewField("logs", nil, []json.RawMessage{}), - data.NewField("references", nil, []json.RawMessage{}), - data.NewField("tags", nil, []json.RawMessage{}), - data.NewField("warnings", nil, []json.RawMessage{}), - data.NewField("stackTraces", nil, []json.RawMessage{}), - ) - - // Set metadata for trace visualization - frame.Meta = &data.FrameMeta{ - PreferredVisualization: "trace", - Custom: map[string]interface{}{ - "traceFormat": "jaeger", - }, - } - - // Process each span in the trace - for _, span := range trace.Spans { - // Find parent span ID - var parentSpanID *string - for _, ref := range span.References { - if ref.RefType == "CHILD_OF" { - s := ref.SpanID - parentSpanID = &s - break - } - } - - // Get service name and tags - serviceName := "" - serviceTags := json.RawMessage{} - if process, ok := trace.Processes[span.ProcessID]; ok { - serviceName = process.ServiceName - tagsMarshaled, err := json.Marshal(process.Tags) - if err == nil { - serviceTags = json.RawMessage(tagsMarshaled) - } - } - - // Convert logs - logs := json.RawMessage{} - logsMarshaled, err := json.Marshal(span.Logs) - if err == nil { - logs = json.RawMessage(logsMarshaled) - } - - // Convert references (excluding parent) - references := json.RawMessage{} - filteredRefs := []TraceSpanReference{} - for _, ref := range span.References { - if parentSpanID == nil || ref.SpanID != *parentSpanID { - filteredRefs = append(filteredRefs, ref) - } - } - refsMarshaled, err := json.Marshal(filteredRefs) - if err == nil { - references = json.RawMessage(refsMarshaled) - } - - // Convert tags - tags := json.RawMessage{} - tagsMarshaled, err := json.Marshal(span.Tags) - if err == nil { - tags = json.RawMessage(tagsMarshaled) - } - - // Convert warnings - warnings := json.RawMessage{} - warningsMarshaled, err := json.Marshal(span.Warnings) - if err == nil { - warnings = json.RawMessage(warningsMarshaled) - } - - // Convert stack traces - stackTraces := json.RawMessage{} - stackTracesMarshaled, err := json.Marshal(span.StackTraces) - if err == nil { - stackTraces = json.RawMessage(stackTracesMarshaled) - } - - // Add span to frame - frame.AppendRow( - span.TraceID, - span.SpanID, - parentSpanID, - span.OperationName, - serviceName, - serviceTags, - float64(span.StartTime)/1000, // Convert microseconds to milliseconds - float64(span.Duration)/1000, // Convert microseconds to milliseconds - logs, - references, - tags, - warnings, - stackTraces, - ) - } - - return frame -} - -func transformDependenciesResponse(dependencies DependenciesResponse, refID string) []*data.Frame { +func transformDependenciesResponse(dependencies types.DependenciesResponse, refID string) []*data.Frame { // Create nodes frame nodesFrame := data.NewFrame(refID+"_nodes", data.NewField("id", nil, []string{}), @@ -361,58 +167,3 @@ func transformDependenciesResponse(dependencies DependenciesResponse, refID stri return []*data.Frame{nodesFrame, edgesFrame} } - -type TraceKeyValuePair struct { - Key string `json:"key"` - Type string `json:"type"` - Value interface{} `json:"value"` -} - -type TraceProcess struct { - ServiceName string `json:"serviceName"` - Tags []TraceKeyValuePair `json:"tags"` -} - -type TraceSpanReference struct { - RefType string `json:"refType"` - SpanID string `json:"spanID"` - TraceID string `json:"traceID"` -} - -type TraceLog struct { - // Millisecond epoch time - Timestamp int64 `json:"timestamp"` - Fields []TraceKeyValuePair `json:"fields"` - Name string `json:"name"` -} - -type Span struct { - TraceID string `json:"traceID"` - SpanID string `json:"spanID"` - ProcessID string `json:"processID"` - OperationName string `json:"operationName"` - // Times are in microseconds - StartTime int64 `json:"startTime"` - Duration int64 `json:"duration"` - Logs []TraceLog `json:"logs"` - References []TraceSpanReference `json:"references"` - Tags []TraceKeyValuePair `json:"tags"` - Warnings []string `json:"warnings"` - Flags int `json:"flags"` - StackTraces []string `json:"stackTraces"` -} - -type TraceResponse struct { - Processes map[string]TraceProcess `json:"processes"` - TraceID string `json:"traceID"` - Warnings []string `json:"warnings"` - Spans []Span `json:"spans"` -} - -type TracesResponse struct { - Data []TraceResponse `json:"data"` - Errors interface{} `json:"errors"` // TODO: Handle errors, but we were not using them in the frontend either - Limit int `json:"limit"` - Offset int `json:"offset"` - Total int `json:"total"` -} diff --git a/pkg/tsdb/jaeger/querydata_test.go b/pkg/tsdb/jaeger/querydata_test.go index baa833ba07c..05b1b01b210 100644 --- a/pkg/tsdb/jaeger/querydata_test.go +++ b/pkg/tsdb/jaeger/querydata_test.go @@ -3,294 +3,14 @@ package jaeger import ( "testing" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" ) -func TestTransformSearchResponse(t *testing.T) { - t.Run("empty_response", func(t *testing.T) { - dsInfo := &datasourceInfo{ - JaegerClient: JaegerClient{ - settings: backend.DataSourceInstanceSettings{ - UID: "test-uid", - Name: "test-name", - }, - }, - } - - frame := transformSearchResponse([]TraceResponse{}, dsInfo) - experimental.CheckGoldenJSONFrame(t, "./testdata", "search_empty_response.golden", frame, false) - }) - - t.Run("single_trace", func(t *testing.T) { - dsInfo := &datasourceInfo{ - JaegerClient: JaegerClient{ - settings: backend.DataSourceInstanceSettings{ - UID: "test-uid", - Name: "test-name", - }, - }, - } - - response := []TraceResponse{ - { - TraceID: "test-trace-id", - Spans: []Span{ - { - TraceID: "test-trace-id", - ProcessID: "p1", - OperationName: "test-operation", - StartTime: 1605873894680409, - Duration: 1000, - }, - }, - Processes: map[string]TraceProcess{ - "p1": { - ServiceName: "test-service", - }, - }, - }, - } - - frame := transformSearchResponse(response, dsInfo) - experimental.CheckGoldenJSONFrame(t, "./testdata", "search_single_response.golden", frame, false) - }) - - t.Run("multiple_traces", func(t *testing.T) { - dsInfo := &datasourceInfo{ - JaegerClient: JaegerClient{ - settings: backend.DataSourceInstanceSettings{ - UID: "test-uid", - Name: "test-name", - }, - }, - } - - response := []TraceResponse{ - { - TraceID: "trace-1", - Spans: []Span{ - { - TraceID: "trace-1", - ProcessID: "p1", - OperationName: "op1", - StartTime: 1605873894680409, - Duration: 1000, - }, - }, - Processes: map[string]TraceProcess{ - "p1": { - ServiceName: "service-1", - }, - }, - }, - { - TraceID: "trace-2", - Spans: []Span{ - { - TraceID: "trace-2", - ProcessID: "p2", - OperationName: "op2", - StartTime: 1605873894680409, - Duration: 2000, - }, - }, - Processes: map[string]TraceProcess{ - "p2": { - ServiceName: "service-2", - }, - }, - }, - } - - frame := transformSearchResponse(response, dsInfo) - experimental.CheckGoldenJSONFrame(t, "./testdata", "search_multiple_response.golden", frame, false) - }) -} - -func TestTransformTraceResponse(t *testing.T) { - t.Run("simple_trace", func(t *testing.T) { - trace := TraceResponse{ - TraceID: "3fa414edcef6ad90", - Spans: []Span{ - { - TraceID: "3fa414edcef6ad90", - SpanID: "3fa414edcef6ad90", - OperationName: "HTTP GET - api_traces_traceid", - StartTime: 1605873894680409, - Duration: 1049141, - Tags: []TraceKeyValuePair{ - {Key: "sampler.type", Type: "string", Value: "probabilistic"}, - {Key: "sampler.param", Type: "float64", Value: 1}, - }, - Logs: []TraceLog{}, - ProcessID: "p1", - Warnings: nil, - Flags: 0, - }, - { - TraceID: "3fa414edcef6ad90", - SpanID: "0f5c1808567e4403", - OperationName: "/tempopb.Querier/FindTraceByID", - References: []TraceSpanReference{ - { - RefType: "CHILD_OF", - TraceID: "3fa414edcef6ad90", - SpanID: "3fa414edcef6ad90", - }, - }, - StartTime: 1605873894680587, - Duration: 1847, - Tags: []TraceKeyValuePair{ - {Key: "component", Type: "string", Value: "gRPC"}, - {Key: "span.kind", Type: "string", Value: "client"}, - }, - Logs: []TraceLog{}, - ProcessID: "p1", - Warnings: nil, - Flags: 0, - }, - }, - Processes: map[string]TraceProcess{ - "p1": { - ServiceName: "tempo-querier", - Tags: []TraceKeyValuePair{ - {Key: "cluster", Type: "string", Value: "ops-tools1"}, - {Key: "container", Type: "string", Value: "tempo-query"}, - }, - }, - }, - Warnings: nil, - } - - frame := transformTraceResponse(trace, "test") - experimental.CheckGoldenJSONFrame(t, "./testdata", "simple_trace.golden", frame, false) - }) - - t.Run("complex_trace", func(t *testing.T) { - trace := TraceResponse{ - TraceID: "3fa414edcef6ad90", - Spans: []Span{ - { - TraceID: "3fa414edcef6ad90", - SpanID: "3fa414edcef6ad90", - OperationName: "HTTP GET - api_traces_traceid", - References: []TraceSpanReference{}, - StartTime: 1605873894680409, - Duration: 1049141, - Tags: []TraceKeyValuePair{ - {Key: "sampler.type", Type: "string", Value: "probabilistic"}, - {Key: "sampler.param", Type: "float64", Value: 1}, - {Key: "error", Type: "bool", Value: true}, - {Key: "http.status_code", Type: "int", Value: 500}, - }, - Logs: []TraceLog{ - { - Timestamp: 1605873894681000, - Fields: []TraceKeyValuePair{ - {Key: "event", Type: "string", Value: "error"}, - {Key: "message", Type: "string", Value: "Internal server error"}, - }, - }, - }, - ProcessID: "p1", - Warnings: []string{"High latency detected", "Error rate above threshold"}, - Flags: 0, - }, - { - TraceID: "3fa414edcef6ad90", - SpanID: "0f5c1808567e4403", - OperationName: "/tempopb.Querier/FindTraceByID", - References: []TraceSpanReference{ - { - RefType: "CHILD_OF", - TraceID: "3fa414edcef6ad90", - SpanID: "3fa414edcef6ad90", - }, - }, - StartTime: 1605873894680587, - Duration: 1847, - Tags: []TraceKeyValuePair{ - {Key: "component", Type: "string", Value: "gRPC"}, - {Key: "span.kind", Type: "string", Value: "client"}, - {Key: "error", Type: "bool", Value: true}, - {Key: "grpc.status_code", Type: "int", Value: 13}, - }, - Logs: []TraceLog{ - { - Timestamp: 1605873894680700, - Fields: []TraceKeyValuePair{ - {Key: "event", Type: "string", Value: "error"}, - {Key: "message", Type: "string", Value: "gRPC error: INTERNAL"}, - }, - }, - }, - ProcessID: "p1", - Warnings: []string{"gRPC call failed", "Retry attempt 3"}, - Flags: 0, - }, - { - TraceID: "3fa414edcef6ad90", - SpanID: "1a2b3c4d5e6f7g8h", - OperationName: "db.query", - References: []TraceSpanReference{ - { - RefType: "CHILD_OF", - TraceID: "3fa414edcef6ad90", - SpanID: "0f5c1808567e4403", - }, - }, - StartTime: 1605873894680800, - Duration: 500, - Tags: []TraceKeyValuePair{ - {Key: "db.type", Type: "string", Value: "postgresql"}, - {Key: "db.statement", Type: "string", Value: "SELECT * FROM traces WHERE id = $1"}, - {Key: "error", Type: "bool", Value: true}, - }, - Logs: []TraceLog{ - { - Timestamp: 1605873894680850, - Fields: []TraceKeyValuePair{ - {Key: "event", Type: "string", Value: "error"}, - {Key: "message", Type: "string", Value: "Database connection timeout"}, - }, - }, - }, - ProcessID: "p2", - Warnings: []string{"Database connection slow", "Query timeout"}, - Flags: 0, - }, - }, - Processes: map[string]TraceProcess{ - "p1": { - ServiceName: "tempo-querier", - Tags: []TraceKeyValuePair{ - {Key: "cluster", Type: "string", Value: "ops-tools1"}, - {Key: "container", Type: "string", Value: "tempo-query"}, - {Key: "version", Type: "string", Value: "1.2.3"}, - }, - }, - "p2": { - ServiceName: "tempo-storage", - Tags: []TraceKeyValuePair{ - {Key: "cluster", Type: "string", Value: "ops-tools1"}, - {Key: "container", Type: "string", Value: "tempo-storage"}, - {Key: "version", Type: "string", Value: "2.0.1"}, - }, - }, - }, - Warnings: []string{"Trace contains errors", "Multiple service failures"}, - } - - frame := transformTraceResponse(trace, "test") - experimental.CheckGoldenJSONFrame(t, "./testdata", "complex_trace.golden", frame, false) - }) -} - func TestTransformDependenciesResponse(t *testing.T) { t.Run("simple_dependencies", func(t *testing.T) { - dependencies := DependenciesResponse{ - Data: []ServiceDependency{ + dependencies := types.DependenciesResponse{ + Data: []types.ServiceDependency{ { Parent: "serviceA", Child: "serviceB", @@ -315,8 +35,8 @@ func TestTransformDependenciesResponse(t *testing.T) { }) t.Run("empty_dependencies", func(t *testing.T) { - dependencies := DependenciesResponse{ - Data: []ServiceDependency{}, + dependencies := types.DependenciesResponse{ + Data: []types.ServiceDependency{}, } frames := transformDependenciesResponse(dependencies, "test") @@ -325,8 +45,8 @@ func TestTransformDependenciesResponse(t *testing.T) { }) t.Run("complex_dependencies", func(t *testing.T) { - dependencies := DependenciesResponse{ - Data: []ServiceDependency{ + dependencies := types.DependenciesResponse{ + Data: []types.ServiceDependency{ { Parent: "frontend", Child: "auth-service", diff --git a/pkg/tsdb/jaeger/testdata/complex_trace_grpc.golden.jsonc b/pkg/tsdb/jaeger/testdata/complex_trace_grpc.golden.jsonc new file mode 100644 index 00000000000..592cd669121 --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/complex_trace_grpc.golden.jsonc @@ -0,0 +1,404 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "traceFormat": "jaeger" +// }, +// "preferredVisualisationType": "trace" +// } +// Name: test +// Dimensions: 14 Fields by 3 Rows +// +------------------+------------------+--------------------+------------------+---------------------+----------------+--------------------------------+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// | Name: traceID | Name: spanID | Name: parentSpanID | Name: statusCode | Name: statusMessage | Name: kind | Name: operationName | Name: serviceName | Name: serviceTags | Name: startTime | Name: duration | Name: logs | Name: references | Name: tags | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []string | Type: []int64 | Type: []string | Type: []string | Type: []string | Type: []string | Type: []json.RawMessage | Type: []float64 | Type: []float64 | Type: []json.RawMessage | Type: []json.RawMessage | Type: []json.RawMessage | +// +------------------+------------------+--------------------+------------------+---------------------+----------------+--------------------------------+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// | 3fa414edcef6ad90 | 3fa414edcef6ad90 | | 0 | | unspecified | HTTP GET - api_traces_traceid | tempo-querier | [{"key":"service.name","type":"string","value":"tempo-querier"},{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-storage"},{"key":"version","type":"string","value":"2.0.1"}] | 1.6058738946804092e+12 | 1049.140992 | [{"timestamp":1605873894681000,"fields":[{"key":"event","type":"string","value":"error"},{"key":"message","type":"string","value":"Internal server error"}],"name":""}] | [] | [{"key":"sampler.type","type":"string","value":"probabilistic"},{"key":"sampler.param","type":"float64","value":1},{"key":"error","type":"boolean","value":true},{"key":"http.status_code","type":"int64","value":500}] | +// | 3fa414edcef6ad90 | 0f5c1808567e4403 | | 0 | | unspecified | /tempopb.Querier/FindTraceByID | tempo-querier | [{"key":"service.name","type":"string","value":"tempo-querier"},{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-storage"},{"key":"version","type":"string","value":"2.0.1"}] | 1.605873894680587e+12 | 1.84704 | [{"timestamp":1605873894680700,"fields":[{"key":"event","type":"string","value":"error"},{"key":"message","type":"string","value":"gRPC error: INTERNAL"}],"name":""}] | [{"refType":"","spanID":"3fa414edcef6ad90","traceID":"3fa414edcef6ad90"}] | [{"key":"component","type":"string","value":"gRPC"},{"key":"span.kind","type":"string","value":"client"},{"key":"error","type":"boolean","value":true},{"key":"grpc.status_code","type":"int64","value":13}] | +// | 3fa414edcef6ad90 | 1a2b3c4d5e6f7g8h | | 0 | | unspecified | db.query | tempo-storage | [{"key":"service.name","type":"string","value":"tempo-storage"},{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-storage"},{"key":"version","type":"string","value":"2.0.1"}] | 1.6058738946808e+12 | 0.499968 | [{"timestamp":1605873894681000,"fields":[{"key":"event","type":"string","value":"error"},{"key":"message","type":"string","value":"Database connection timeout"}],"name":""}] | [{"refType":"","spanID":"0f5c1808567e4403","traceID":"3fa414edcef6ad90"}] | [{"key":"db.type","type":"string","value":"postgresql"},{"key":"db.statement","type":"string","value":"SELECT * FROM traces WHERE id = $1"},{"key":"error","type":"boolean","value":true}] | +// +------------------+------------------+--------------------+------------------+---------------------+----------------+--------------------------------+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "custom": { + "traceFormat": "jaeger" + }, + "preferredVisualisationType": "trace" + }, + "fields": [ + { + "name": "traceID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "spanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "parentSpanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "statusCode", + "type": "number", + "typeInfo": { + "frame": "int64" + } + }, + { + "name": "statusMessage", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "kind", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "operationName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceTags", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "startTime", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "duration", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "logs", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "references", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "tags", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + } + ] + }, + "data": { + "values": [ + [ + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90" + ], + [ + "3fa414edcef6ad90", + "0f5c1808567e4403", + "1a2b3c4d5e6f7g8h" + ], + [ + "", + "", + "" + ], + [ + 0, + 0, + 0 + ], + [ + "", + "", + "" + ], + [ + "unspecified", + "unspecified", + "unspecified" + ], + [ + "HTTP GET - api_traces_traceid", + "/tempopb.Querier/FindTraceByID", + "db.query" + ], + [ + "tempo-querier", + "tempo-querier", + "tempo-storage" + ], + [ + [ + { + "key": "service.name", + "type": "string", + "value": "tempo-querier" + }, + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-storage" + }, + { + "key": "version", + "type": "string", + "value": "2.0.1" + } + ], + [ + { + "key": "service.name", + "type": "string", + "value": "tempo-querier" + }, + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-storage" + }, + { + "key": "version", + "type": "string", + "value": "2.0.1" + } + ], + [ + { + "key": "service.name", + "type": "string", + "value": "tempo-storage" + }, + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-storage" + }, + { + "key": "version", + "type": "string", + "value": "2.0.1" + } + ] + ], + [ + 1605873894680.4092, + 1605873894680.587, + 1605873894680.8 + ], + [ + 1049.140992, + 1.84704, + 0.499968 + ], + [ + [ + { + "timestamp": 1605873894681000, + "fields": [ + { + "key": "event", + "type": "string", + "value": "error" + }, + { + "key": "message", + "type": "string", + "value": "Internal server error" + } + ], + "name": "" + } + ], + [ + { + "timestamp": 1605873894680700, + "fields": [ + { + "key": "event", + "type": "string", + "value": "error" + }, + { + "key": "message", + "type": "string", + "value": "gRPC error: INTERNAL" + } + ], + "name": "" + } + ], + [ + { + "timestamp": 1605873894681000, + "fields": [ + { + "key": "event", + "type": "string", + "value": "error" + }, + { + "key": "message", + "type": "string", + "value": "Database connection timeout" + } + ], + "name": "" + } + ] + ], + [ + [], + [ + { + "refType": "", + "spanID": "3fa414edcef6ad90", + "traceID": "3fa414edcef6ad90" + } + ], + [ + { + "refType": "", + "spanID": "0f5c1808567e4403", + "traceID": "3fa414edcef6ad90" + } + ] + ], + [ + [ + { + "key": "sampler.type", + "type": "string", + "value": "probabilistic" + }, + { + "key": "sampler.param", + "type": "float64", + "value": 1 + }, + { + "key": "error", + "type": "boolean", + "value": true + }, + { + "key": "http.status_code", + "type": "int64", + "value": 500 + } + ], + [ + { + "key": "component", + "type": "string", + "value": "gRPC" + }, + { + "key": "span.kind", + "type": "string", + "value": "client" + }, + { + "key": "error", + "type": "boolean", + "value": true + }, + { + "key": "grpc.status_code", + "type": "int64", + "value": 13 + } + ], + [ + { + "key": "db.type", + "type": "string", + "value": "postgresql" + }, + { + "key": "db.statement", + "type": "string", + "value": "SELECT * FROM traces WHERE id = $1" + }, + { + "key": "error", + "type": "boolean", + "value": true + } + ] + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/jaeger/testdata/simple_trace_grpc.golden.jsonc b/pkg/tsdb/jaeger/testdata/simple_trace_grpc.golden.jsonc new file mode 100644 index 00000000000..eba7dce41ca --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/simple_trace_grpc.golden.jsonc @@ -0,0 +1,274 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "traceFormat": "jaeger" +// }, +// "preferredVisualisationType": "trace" +// } +// Name: test +// Dimensions: 14 Fields by 2 Rows +// +------------------+------------------+--------------------+------------------+---------------------+----------------+-------------------------------+-------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+-----------------+-------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// | Name: traceID | Name: spanID | Name: parentSpanID | Name: statusCode | Name: statusMessage | Name: kind | Name: operationName | Name: serviceName | Name: serviceTags | Name: startTime | Name: duration | Name: logs | Name: references | Name: tags | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []string | Type: []int64 | Type: []string | Type: []string | Type: []string | Type: []string | Type: []json.RawMessage | Type: []float64 | Type: []float64 | Type: []json.RawMessage | Type: []json.RawMessage | Type: []json.RawMessage | +// +------------------+------------------+--------------------+------------------+---------------------+----------------+-------------------------------+-------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+-----------------+-------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// | 3fa414edcef6ad90 | 3fa414edcef6ad90 | | 0 | | unspecified | HTTP GET - api_traces_traceid | tempo-querier | [{"key":"service.name","type":"string","value":"tempo-querier"},{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-query"}] | 1.6058738946804092e+12 | 1049.140992 | [] | [] | [{"key":"sampler.type","type":"string","value":"probabilistic"},{"key":"sampler.param","type":"float64","value":100},{"key":"otel.scope.name","type":"string","value":"some_scope1"},{"key":"otel.scope.version","type":"string","value":"0.0.39"}] | +// | 3fa414edcef6ad90 | 0f5c1808567e4403 | 3fa414edcef6ad90 | 0 | | unspecified | HTTP GET - api_traces_traceid | tempo-querier | [{"key":"service.name","type":"string","value":"tempo-querier"},{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-query"}] | 1.605873894680587e+12 | 1.84704 | [] | [] | [{"key":"component","type":"string","value":"gRPC"},{"key":"otel.scope.name","type":"string","value":"some_scope1"},{"key":"otel.scope.version","type":"string","value":"0.0.39"}] | +// +------------------+------------------+--------------------+------------------+---------------------+----------------+-------------------------------+-------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+-----------------+-------------------------+-------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "custom": { + "traceFormat": "jaeger" + }, + "preferredVisualisationType": "trace" + }, + "fields": [ + { + "name": "traceID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "spanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "parentSpanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "statusCode", + "type": "number", + "typeInfo": { + "frame": "int64" + } + }, + { + "name": "statusMessage", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "kind", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "operationName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceTags", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "startTime", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "duration", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "logs", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "references", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "tags", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + } + ] + }, + "data": { + "values": [ + [ + "3fa414edcef6ad90", + "3fa414edcef6ad90" + ], + [ + "3fa414edcef6ad90", + "0f5c1808567e4403" + ], + [ + "", + "3fa414edcef6ad90" + ], + [ + 0, + 0 + ], + [ + "", + "" + ], + [ + "unspecified", + "unspecified" + ], + [ + "HTTP GET - api_traces_traceid", + "HTTP GET - api_traces_traceid" + ], + [ + "tempo-querier", + "tempo-querier" + ], + [ + [ + { + "key": "service.name", + "type": "string", + "value": "tempo-querier" + }, + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-query" + } + ], + [ + { + "key": "service.name", + "type": "string", + "value": "tempo-querier" + }, + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-query" + } + ] + ], + [ + 1605873894680.4092, + 1605873894680.587 + ], + [ + 1049.140992, + 1.84704 + ], + [ + [], + [] + ], + [ + [], + [] + ], + [ + [ + { + "key": "sampler.type", + "type": "string", + "value": "probabilistic" + }, + { + "key": "sampler.param", + "type": "float64", + "value": 100 + }, + { + "key": "otel.scope.name", + "type": "string", + "value": "some_scope1" + }, + { + "key": "otel.scope.version", + "type": "string", + "value": "0.0.39" + } + ], + [ + { + "key": "component", + "type": "string", + "value": "gRPC" + }, + { + "key": "otel.scope.name", + "type": "string", + "value": "some_scope1" + }, + { + "key": "otel.scope.version", + "type": "string", + "value": "0.0.39" + } + ] + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/jaeger/types/grpc_types.go b/pkg/tsdb/jaeger/types/grpc_types.go new file mode 100644 index 00000000000..6e4ed417116 --- /dev/null +++ b/pkg/tsdb/jaeger/types/grpc_types.go @@ -0,0 +1,123 @@ +package types + +// gRPC related types as defined in: https://github.com/jaegertracing/jaeger-idl/blob/main/swagger/api_v3/query_service.swagger.json + +type GrpcServicesResponse struct { + Services []string `json:"services"` +} + +type GrpcOperationsResponse struct { + Operations []GrpcOperation `json:"operations"` +} + +type GrpcOperation struct { + Name string `json:"name"` + SpanKind string `json:"spanKind"` +} + +type GrpcTracesResponse struct { + Result GrpcTracesResult `json:"result"` + Error GrpcRuntimeStreamError `json:"error"` +} + +type GrpcRuntimeStreamError struct { + GrpcCode int32 `json:"grpcCode"` + HttpCode int `json:"httpCode"` + Message string `json:"message"` + HttpStatus string `json:"httpStatus"` + Details []ProtobufAny `json:"details"` +} + +type ProtobufAny struct { + TypeUrl string `json:"typeUrl"` + Value string `json:"value"` +} +type GrpcTracesResult struct { + ResourceSpans []GrpcResourceSpans `json:"resourceSpans"` +} +type GrpcResourceSpans struct { + Resource GrpcResource `json:"resource"` + ScopeSpans []GrpcScopeSpans `json:"scopeSpans"` + SchemaURL string `json:"schemaUrl"` +} + +type GrpcScopeSpans struct { + Scope GrpcInstrumentationScope `json:"scope"` + Spans []GrpcSpan `json:"spans"` + SchemaURL string `json:"schemaUrl"` +} + +type GrpcInstrumentationScope struct { + Name string `json:"name"` + Version string `json:"version"` + Attributes []GrpcKeyValue `json:"attributes"` + DroppedAttributesCount int64 `json:"droppedAttributesCount"` +} + +type GrpcSpan struct { + TraceID string `json:"traceId"` + SpanID string `json:"spanId"` + TraceState string `json:"traceState"` + ParentSpanID string `json:"parentSpanId"` + Flags int64 `json:"flags"` + Name string `json:"name"` + Kind int64 `json:"kind"` // default SPAN_KIND_UNSPECIFIED + StartTimeUnixNano string `json:"startTimeUnixNano"` + EndTimeUnixNano string `json:"endTimeUnixNano"` + Attributes []GrpcKeyValue `json:"attributes"` + DroppedAttributesCount int64 `json:"droppedAttributesCount"` + Events []GrpcSpanEvent `json:"events"` + DroppedEventsCount int64 `json:"droppedEventsCount"` + Links []GrpcSpanLink `json:"links"` + DroppedLinksCount int64 `json:"droppedLinksCount"` + Status GrpcStatus `json:"status"` +} + +type GrpcSpanEvent struct { + TimeUnixNano string `json:"timeUnixNano"` + Name string `json:"name"` + Attributes []GrpcKeyValue `json:"attributes"` + DroppedAttributesCount int64 `json:"droppedAttributesCount"` +} + +type GrpcSpanLink struct { + TraceID string `json:"traceId"` + SpanID string `json:"spanId"` + TraceState string `json:"traceState"` + Attributes []GrpcKeyValue `json:"attributes"` + DroppedAttributesCount int64 `json:"droppedAttributesCount"` + Flags int64 `json:"flags"` +} + +type GrpcStatus struct { + Message string `json:"message"` + Code int64 `json:"code"` // default STATUS_CODE_UNSET +} + +type GrpcResource struct { + Attributes []GrpcKeyValue `json:"attributes"` + DroppedAttributesCount int64 `json:"droppedAttributesCount"` +} + +type GrpcKeyValue struct { + Key string `json:"key"` + Value GrpcAnyValue `json:"value"` +} + +type GrpcAnyValue struct { + StringValue string `json:"stringValue"` + BoolValue string `json:"boolValue"` + IntValue string `json:"intValue"` + DoubleValue string `json:"doubleValue"` + ArrayValue GrpcArrayValue `json:"array_value"` + KvListValue KeyValueList `json:"kvlistValue"` + BytesValue string `json:"bytesValue"` +} + +type GrpcArrayValue struct { + Values []GrpcAnyValue `json:"values"` +} + +type KeyValueList struct { + Values []GrpcKeyValue `json:"values"` +} diff --git a/pkg/tsdb/jaeger/types/types.go b/pkg/tsdb/jaeger/types/types.go new file mode 100644 index 00000000000..ba6c948e8b1 --- /dev/null +++ b/pkg/tsdb/jaeger/types/types.go @@ -0,0 +1,85 @@ +package types + +type ServicesResponse struct { + Data []string `json:"data"` + Errors interface{} `json:"errors"` + Limit int `json:"limit"` + Offset int `json:"offset"` + Total int `json:"total"` +} + +type SettingsJSONData struct { + TraceIdTimeParams struct { + Enabled bool `json:"enabled"` + } `json:"traceIdTimeParams"` +} + +type DependenciesResponse struct { + Data []ServiceDependency `json:"data"` + Errors []struct { + Code int `json:"code"` + Msg string `json:"msg"` + } `json:"errors"` +} + +type ServiceDependency struct { + Parent string `json:"parent"` + Child string `json:"child"` + CallCount int `json:"callCount"` +} + +type KeyValueType struct { + Key string `json:"key"` + Type string `json:"type"` + Value interface{} `json:"value"` +} + +type TraceProcess struct { + ServiceName string `json:"serviceName"` + Tags []KeyValueType `json:"tags"` +} + +type TraceSpanReference struct { + // RefType is not supported for OTLP-based traces and may be empty. + RefType string `json:"refType"` + SpanID string `json:"spanID"` + TraceID string `json:"traceID"` +} + +type TraceLog struct { + // Millisecond epoch time + Timestamp int64 `json:"timestamp"` + Fields []KeyValueType `json:"fields"` + Name string `json:"name"` +} + +type Span struct { + TraceID string `json:"traceID"` + SpanID string `json:"spanID"` + ProcessID string `json:"processID"` + OperationName string `json:"operationName"` + // Times are in microseconds + StartTime int64 `json:"startTime"` + Duration int64 `json:"duration"` + Logs []TraceLog `json:"logs"` + References []TraceSpanReference `json:"references"` + Tags []KeyValueType `json:"tags"` + Warnings []string `json:"warnings"` + Flags int `json:"flags"` + StackTraces []string `json:"stackTraces"` +} + +type TraceResponse struct { + Processes map[string]TraceProcess `json:"processes"` + TraceID string `json:"traceID"` + Warnings []string `json:"warnings"` + Spans []Span `json:"spans"` +} + +type TracesResponse struct { + Data []TraceResponse `json:"data"` + Errors interface{} `json:"errors"` // TODO: Handle errors, but we were not using them in the frontend either + Limit int `json:"limit"` + Offset int `json:"offset"` + Total int `json:"total"` +} diff --git a/pkg/tsdb/jaeger/utils/client_utils.go b/pkg/tsdb/jaeger/utils/client_utils.go new file mode 100644 index 00000000000..c43a16a3ef5 --- /dev/null +++ b/pkg/tsdb/jaeger/utils/client_utils.go @@ -0,0 +1,213 @@ +package utils + +import ( + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" +) + +func TransformSearchResponse(response []types.TraceResponse, dsUID string, dsName string) *data.Frame { + // Create a frame for the traces + frame := data.NewFrame("traces", + data.NewField("traceID", nil, []string{}).SetConfig(&data.FieldConfig{ + DisplayName: "Trace ID", + Links: []data.DataLink{ + { + Title: "Trace: ${__value.raw}", + URL: "", + Internal: &data.InternalDataLink{ + DatasourceUID: dsUID, + DatasourceName: dsName, + Query: map[string]interface{}{ + "query": "${__value.raw}", + }, + }, + }, + }, + }), + data.NewField("traceName", nil, []string{}).SetConfig(&data.FieldConfig{ + DisplayName: "Trace name", + }), + data.NewField("startTime", nil, []time.Time{}).SetConfig(&data.FieldConfig{ + DisplayName: "Start time", + }), + data.NewField("duration", nil, []int64{}).SetConfig(&data.FieldConfig{ + DisplayName: "Duration", + Unit: "µs", + }), + ) + + // Set the visualization type to table + frame.Meta = &data.FrameMeta{ + PreferredVisualization: "table", + } + + // Sort traces by start time in descending order (newest first) + sort.Slice(response, func(i, j int) bool { + rootSpanI := response[i].Spans[0] + rootSpanJ := response[j].Spans[0] + + for _, span := range response[i].Spans { + if span.StartTime < rootSpanI.StartTime { + rootSpanI = span + } + } + + for _, span := range response[j].Spans { + if span.StartTime < rootSpanJ.StartTime { + rootSpanJ = span + } + } + + return rootSpanI.StartTime > rootSpanJ.StartTime + }) + + // Process each trace + for _, trace := range response { + if len(trace.Spans) == 0 { + continue + } + + // Get the root span + rootSpan := trace.Spans[0] + for _, span := range trace.Spans { + if span.StartTime < rootSpan.StartTime { + rootSpan = span + } + } + + // Get the service name for the trace + serviceName := "" + if process, ok := trace.Processes[rootSpan.ProcessID]; ok { + serviceName = process.ServiceName + } + + // Get the trace name and start time + traceName := fmt.Sprintf("%s: %s", serviceName, rootSpan.OperationName) + startTime := time.Unix(0, rootSpan.StartTime*1000) + + // Append the row to the frame + frame.AppendRow( + trace.TraceID, + traceName, + startTime, + rootSpan.Duration, + ) + } + + return frame +} + +func TransformTraceResponse(trace types.TraceResponse, refID string) *data.Frame { + frame := data.NewFrame(refID, + data.NewField("traceID", nil, []string{}), + data.NewField("spanID", nil, []string{}), + data.NewField("parentSpanID", nil, []*string{}), + data.NewField("operationName", nil, []string{}), + data.NewField("serviceName", nil, []string{}), + data.NewField("serviceTags", nil, []json.RawMessage{}), + data.NewField("startTime", nil, []float64{}), + data.NewField("duration", nil, []float64{}), + data.NewField("logs", nil, []json.RawMessage{}), + data.NewField("references", nil, []json.RawMessage{}), + data.NewField("tags", nil, []json.RawMessage{}), + data.NewField("warnings", nil, []json.RawMessage{}), + data.NewField("stackTraces", nil, []json.RawMessage{}), + ) + + // Set metadata for trace visualization + frame.Meta = &data.FrameMeta{ + PreferredVisualization: "trace", + Custom: map[string]interface{}{ + "traceFormat": "jaeger", + }, + } + + // Process each span in the trace + for _, span := range trace.Spans { + // Find parent span ID + var parentSpanID *string + for _, ref := range span.References { + if ref.RefType == "CHILD_OF" { + s := ref.SpanID + parentSpanID = &s + break + } + } + + // Get service name and tags + serviceName := "" + serviceTags := json.RawMessage{} + if process, ok := trace.Processes[span.ProcessID]; ok { + serviceName = process.ServiceName + tagsMarshaled, err := json.Marshal(process.Tags) + if err == nil { + serviceTags = json.RawMessage(tagsMarshaled) + } + } + + // Convert logs + logs := json.RawMessage{} + logsMarshaled, err := json.Marshal(span.Logs) + if err == nil { + logs = json.RawMessage(logsMarshaled) + } + + // Convert references (excluding parent) + references := json.RawMessage{} + filteredRefs := []types.TraceSpanReference{} + for _, ref := range span.References { + if parentSpanID == nil || ref.SpanID != *parentSpanID { + filteredRefs = append(filteredRefs, ref) + } + } + refsMarshaled, err := json.Marshal(filteredRefs) + if err == nil { + references = json.RawMessage(refsMarshaled) + } + + // Convert tags + tags := json.RawMessage{} + tagsMarshaled, err := json.Marshal(span.Tags) + if err == nil { + tags = json.RawMessage(tagsMarshaled) + } + + // Convert warnings + warnings := json.RawMessage{} + warningsMarshaled, err := json.Marshal(span.Warnings) + if err == nil { + warnings = json.RawMessage(warningsMarshaled) + } + + // Convert stack traces + stackTraces := json.RawMessage{} + stackTracesMarshaled, err := json.Marshal(span.StackTraces) + if err == nil { + stackTraces = json.RawMessage(stackTracesMarshaled) + } + + // Add span to frame + frame.AppendRow( + span.TraceID, + span.SpanID, + parentSpanID, + span.OperationName, + serviceName, + serviceTags, + float64(span.StartTime)/1000, // Convert microseconds to milliseconds + float64(span.Duration)/1000, // Convert microseconds to milliseconds + logs, + references, + tags, + warnings, + stackTraces, + ) + } + + return frame +} diff --git a/pkg/tsdb/jaeger/utils/client_utils_test.go b/pkg/tsdb/jaeger/utils/client_utils_test.go new file mode 100644 index 00000000000..55abcc5ed72 --- /dev/null +++ b/pkg/tsdb/jaeger/utils/client_utils_test.go @@ -0,0 +1,261 @@ +package utils + +import ( + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" +) + +func TestTransformSearchResponse(t *testing.T) { + t.Run("empty_response", func(t *testing.T) { + frame := TransformSearchResponse([]types.TraceResponse{}, "test-uid", "test-name") + experimental.CheckGoldenJSONFrame(t, "../testdata", "search_empty_response.golden", frame, false) + }) + + t.Run("single_trace", func(t *testing.T) { + response := []types.TraceResponse{ + { + TraceID: "test-trace-id", + Spans: []types.Span{ + { + TraceID: "test-trace-id", + ProcessID: "p1", + OperationName: "test-operation", + StartTime: 1605873894680409, + Duration: 1000, + }, + }, + Processes: map[string]types.TraceProcess{ + "p1": { + ServiceName: "test-service", + }, + }, + }, + } + + frame := TransformSearchResponse(response, "test-uid", "test-name") + experimental.CheckGoldenJSONFrame(t, "../testdata", "search_single_response.golden", frame, false) + }) + + t.Run("multiple_traces", func(t *testing.T) { + response := []types.TraceResponse{ + { + TraceID: "trace-1", + Spans: []types.Span{ + { + TraceID: "trace-1", + ProcessID: "p1", + OperationName: "op1", + StartTime: 1605873894680409, + Duration: 1000, + }, + }, + Processes: map[string]types.TraceProcess{ + "p1": { + ServiceName: "service-1", + }, + }, + }, + { + TraceID: "trace-2", + Spans: []types.Span{ + { + TraceID: "trace-2", + ProcessID: "p2", + OperationName: "op2", + StartTime: 1605873894680409, + Duration: 2000, + }, + }, + Processes: map[string]types.TraceProcess{ + "p2": { + ServiceName: "service-2", + }, + }, + }, + } + + frame := TransformSearchResponse(response, "test-uid", "test-name") + experimental.CheckGoldenJSONFrame(t, "../testdata", "search_multiple_response.golden", frame, false) + }) +} + +func TestTransformTraceResponse(t *testing.T) { + t.Run("simple_trace", func(t *testing.T) { + trace := types.TraceResponse{ + TraceID: "3fa414edcef6ad90", + Spans: []types.Span{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + OperationName: "HTTP GET - api_traces_traceid", + StartTime: 1605873894680409, + Duration: 1049141, + Tags: []types.KeyValueType{ + {Key: "sampler.type", Type: "string", Value: "probabilistic"}, + {Key: "sampler.param", Type: "float64", Value: 1}, + }, + Logs: []types.TraceLog{}, + ProcessID: "p1", + Warnings: nil, + Flags: 0, + }, + { + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + OperationName: "/tempopb.Querier/FindTraceByID", + References: []types.TraceSpanReference{ + { + RefType: "CHILD_OF", + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + }, + }, + StartTime: 1605873894680587, + Duration: 1847, + Tags: []types.KeyValueType{ + {Key: "component", Type: "string", Value: "gRPC"}, + {Key: "span.kind", Type: "string", Value: "client"}, + }, + Logs: []types.TraceLog{}, + ProcessID: "p1", + Warnings: nil, + Flags: 0, + }, + }, + Processes: map[string]types.TraceProcess{ + "p1": { + ServiceName: "tempo-querier", + Tags: []types.KeyValueType{ + {Key: "cluster", Type: "string", Value: "ops-tools1"}, + {Key: "container", Type: "string", Value: "tempo-query"}, + }, + }, + }, + Warnings: nil, + } + + frame := TransformTraceResponse(trace, "test") + experimental.CheckGoldenJSONFrame(t, "../testdata", "simple_trace.golden", frame, false) + }) + + t.Run("complex_trace", func(t *testing.T) { + trace := types.TraceResponse{ + TraceID: "3fa414edcef6ad90", + Spans: []types.Span{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + OperationName: "HTTP GET - api_traces_traceid", + References: []types.TraceSpanReference{}, + StartTime: 1605873894680409, + Duration: 1049141, + Tags: []types.KeyValueType{ + {Key: "sampler.type", Type: "string", Value: "probabilistic"}, + {Key: "sampler.param", Type: "float64", Value: 1}, + {Key: "error", Type: "bool", Value: true}, + {Key: "http.status_code", Type: "int", Value: 500}, + }, + Logs: []types.TraceLog{ + { + Timestamp: 1605873894681000, + Fields: []types.KeyValueType{ + {Key: "event", Type: "string", Value: "error"}, + {Key: "message", Type: "string", Value: "Internal server error"}, + }, + }, + }, + ProcessID: "p1", + Warnings: []string{"High latency detected", "Error rate above threshold"}, + Flags: 0, + }, + { + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + OperationName: "/tempopb.Querier/FindTraceByID", + References: []types.TraceSpanReference{ + { + RefType: "CHILD_OF", + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + }, + }, + StartTime: 1605873894680587, + Duration: 1847, + Tags: []types.KeyValueType{ + {Key: "component", Type: "string", Value: "gRPC"}, + {Key: "span.kind", Type: "string", Value: "client"}, + {Key: "error", Type: "bool", Value: true}, + {Key: "grpc.status_code", Type: "int", Value: 13}, + }, + Logs: []types.TraceLog{ + { + Timestamp: 1605873894680700, + Fields: []types.KeyValueType{ + {Key: "event", Type: "string", Value: "error"}, + {Key: "message", Type: "string", Value: "gRPC error: INTERNAL"}, + }, + }, + }, + ProcessID: "p1", + Warnings: []string{"gRPC call failed", "Retry attempt 3"}, + Flags: 0, + }, + { + TraceID: "3fa414edcef6ad90", + SpanID: "1a2b3c4d5e6f7g8h", + OperationName: "db.query", + References: []types.TraceSpanReference{ + { + RefType: "CHILD_OF", + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + }, + }, + StartTime: 1605873894680800, + Duration: 500, + Tags: []types.KeyValueType{ + {Key: "db.type", Type: "string", Value: "postgresql"}, + {Key: "db.statement", Type: "string", Value: "SELECT * FROM traces WHERE id = $1"}, + {Key: "error", Type: "bool", Value: true}, + }, + Logs: []types.TraceLog{ + { + Timestamp: 1605873894680850, + Fields: []types.KeyValueType{ + {Key: "event", Type: "string", Value: "error"}, + {Key: "message", Type: "string", Value: "Database connection timeout"}, + }, + }, + }, + ProcessID: "p2", + Warnings: []string{"Database connection slow", "Query timeout"}, + Flags: 0, + }, + }, + Processes: map[string]types.TraceProcess{ + "p1": { + ServiceName: "tempo-querier", + Tags: []types.KeyValueType{ + {Key: "cluster", Type: "string", Value: "ops-tools1"}, + {Key: "container", Type: "string", Value: "tempo-query"}, + {Key: "version", Type: "string", Value: "1.2.3"}, + }, + }, + "p2": { + ServiceName: "tempo-storage", + Tags: []types.KeyValueType{ + {Key: "cluster", Type: "string", Value: "ops-tools1"}, + {Key: "container", Type: "string", Value: "tempo-storage"}, + {Key: "version", Type: "string", Value: "2.0.1"}, + }, + }, + }, + Warnings: []string{"Trace contains errors", "Multiple service failures"}, + } + + frame := TransformTraceResponse(trace, "test") + experimental.CheckGoldenJSONFrame(t, "../testdata", "complex_trace.golden", frame, false) + }) +} diff --git a/pkg/tsdb/jaeger/utils/grpc_utils.go b/pkg/tsdb/jaeger/utils/grpc_utils.go new file mode 100644 index 00000000000..771988450b4 --- /dev/null +++ b/pkg/tsdb/jaeger/utils/grpc_utils.go @@ -0,0 +1,382 @@ +package utils + +import ( + "encoding/json" + "fmt" + "sort" + "strconv" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" +) + +func TransformGrpcSearchResponse(response types.GrpcTracesResult, dsUID string, dsName string, limit int) *data.Frame { + // Create a frame for the traces + frame := data.NewFrame("traces", + data.NewField("traceID", nil, []string{}).SetConfig(&data.FieldConfig{ + DisplayName: "Trace ID", + Links: []data.DataLink{ + { + Title: "Trace: ${__value.raw}", + URL: "", + Internal: &data.InternalDataLink{ + DatasourceUID: dsUID, + DatasourceName: dsName, + Query: map[string]interface{}{ + "query": "${__value.raw}", + }, + }, + }, + }, + }), + data.NewField("traceName", nil, []string{}).SetConfig(&data.FieldConfig{ + DisplayName: "Trace name", + }), + data.NewField("startTime", nil, []time.Time{}).SetConfig(&data.FieldConfig{ + DisplayName: "Start time", + }), + data.NewField("duration", nil, []int64{}).SetConfig(&data.FieldConfig{ + DisplayName: "Duration", + Unit: "µs", + }), + ) + + // Set the visualization type to table + frame.Meta = &data.FrameMeta{ + PreferredVisualization: "table", + } + + // Sort traces by start time in descending order (newest first) + resourceSpans := response.ResourceSpans + sort.Slice(resourceSpans, func(i, j int) bool { + rootSpanI := resourceSpans[i].ScopeSpans[0].Spans[0] + rootSpanJ := resourceSpans[j].ScopeSpans[0].Spans[0] + + for _, scopeSpan := range resourceSpans[i].ScopeSpans { + for _, span := range scopeSpan.Spans { + if span.StartTimeUnixNano < rootSpanI.StartTimeUnixNano { + rootSpanI = span + } + } + } + + for _, scopeSpan := range resourceSpans[j].ScopeSpans { + for _, span := range scopeSpan.Spans { + if span.StartTimeUnixNano < rootSpanJ.StartTimeUnixNano { + rootSpanJ = span + } + } + } + + return rootSpanI.StartTimeUnixNano > rootSpanJ.StartTimeUnixNano + }) + + if limit > 0 { + resourceSpans = resourceSpans[:limit] + } + // process each individual resource + for _, res := range resourceSpans { + serviceName := getAttribute(res.Resource.Attributes, "service.name") + for _, scopeSpan := range res.ScopeSpans { + if len(scopeSpan.Spans) == 0 { + continue + } + + // Get the root span + rootSpan := scopeSpan.Spans[0] + for _, span := range scopeSpan.Spans { + if span.StartTimeUnixNano < rootSpan.StartTimeUnixNano { + rootSpan = span + } + } + + // get trace name + traceName := fmt.Sprintf("%s: %s", serviceName.StringValue, rootSpan.Name) + startTimeInt, startErr := strconv.ParseInt(rootSpan.StartTimeUnixNano, 10, 64) + endTimeInt, endErr := strconv.ParseInt(rootSpan.EndTimeUnixNano, 10, 64) + duration := int64(0) + if startErr == nil && endErr == nil { + duration = (endTimeInt - startTimeInt) / 1000 // convert to microseconds + } + + frame.AppendRow( + rootSpan.TraceID, + traceName, + time.Unix(0, startTimeInt), + duration, + ) + } + } + + return frame +} + +func TransformGrpcTraceResponse(trace []types.GrpcResourceSpans, refID string) *data.Frame { + frame := data.NewFrame(refID, + data.NewField("traceID", nil, []string{}), + data.NewField("spanID", nil, []string{}), + data.NewField("parentSpanID", nil, []string{}), + data.NewField("statusCode", nil, []int64{}), + data.NewField("statusMessage", nil, []string{}), + data.NewField("kind", nil, []string{}), + data.NewField("operationName", nil, []string{}), + data.NewField("serviceName", nil, []string{}), + data.NewField("serviceTags", nil, []json.RawMessage{}), + data.NewField("startTime", nil, []float64{}), + data.NewField("duration", nil, []float64{}), + data.NewField("logs", nil, []json.RawMessage{}), + data.NewField("references", nil, []json.RawMessage{}), + data.NewField("tags", nil, []json.RawMessage{}), + ) + + // Set metadata for trace visualization + frame.Meta = &data.FrameMeta{ + PreferredVisualization: "trace", + Custom: map[string]interface{}{ + "traceFormat": "jaeger", + }, + } + + // each resource is a difference service name or "process" + for _, resource := range trace { + for _, scopeSpan := range resource.ScopeSpans { + for _, span := range scopeSpan.Spans { + parentSpanID := span.ParentSpanID + // Get service name and tags + serviceName := getAttribute(resource.Resource.Attributes, "service.name").StringValue + serviceTags := json.RawMessage{} + processedResAttributes := processAttributes(resource.Resource.Attributes) + tagsMarshaled, err := json.Marshal(processedResAttributes) + if err == nil { + serviceTags = json.RawMessage(tagsMarshaled) + } + + // Convert tags + tags := json.RawMessage{} + processedSpanAttributes := processAttributes(span.Attributes) + // add otel attributes scope name, scope version and span kind + if scopeSpan.Scope.Name != "" { + processedSpanAttributes = append(processedSpanAttributes, types.KeyValueType{ + Key: "otel.scope.name", + Value: scopeSpan.Scope.Name, + Type: "string", + }) + } + + if scopeSpan.Scope.Version != "" { + processedSpanAttributes = append(processedSpanAttributes, types.KeyValueType{ + Key: "otel.scope.version", + Value: scopeSpan.Scope.Version, + Type: "string", + }) + } + + tagsMarshaled, err = json.Marshal(processedSpanAttributes) + if err == nil { + tags = json.RawMessage(tagsMarshaled) + } + + // Convert logs + // In the new API (OTLP based), logs are span events. See: + // https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L630C9-L636C11 + logs := json.RawMessage{} + processedEvents := convertGrpcEventsToLogs(span.Events) + logsMarshaled, err := json.Marshal(processedEvents) + if err == nil { + logs = json.RawMessage(logsMarshaled) + } + + // Convert references (excluding parent) + references := json.RawMessage{} + filteredLinks := []types.GrpcSpanLink{} + // in the new API (OTLP based), references are defined as "SpanLinks" see: + // https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L642C8-L648C11 + for _, ref := range span.Links { + if parentSpanID == "" || ref.SpanID != parentSpanID { + filteredLinks = append(filteredLinks, ref) + } + } + processedLinks := convertGrpcLinkToReference(filteredLinks) + refsMarshaled, err := json.Marshal(processedLinks) + if err == nil { + references = json.RawMessage(refsMarshaled) + } + + // convert start time and calculate duration + startTimeFloat, startErr := strconv.ParseFloat(span.StartTimeUnixNano, 64) + endTimeFloat, endErr := strconv.ParseFloat(span.EndTimeUnixNano, 64) + duration := float64(0) + if startErr == nil && endErr == nil { + duration = (endTimeFloat - startTimeFloat) / 1000000 // convert to milliseconds + } + + // Add span to frame + frame.AppendRow( + span.TraceID, + span.SpanID, + parentSpanID, + span.Status.Code, + span.Status.Message, + processSpanKind(span.Kind), + span.Name, + serviceName, + serviceTags, + startTimeFloat/1000000, // Convert nanoseconds to milliseconds + duration, + logs, + references, + tags, + ) + } + } + } + + return frame +} + +func processAttributes(attributes []types.GrpcKeyValue) []types.KeyValueType { + tags := []types.KeyValueType{} + + for _, att := range attributes { + if att.Value.StringValue != "" { + tags = append(tags, types.KeyValueType{ + Key: att.Key, + Value: att.Value.StringValue, + Type: "string", + }) + continue + } + + if att.Value.BoolValue != "" { + boolVal, err := strconv.ParseBool(att.Value.BoolValue) + if err != nil { + continue + } + tags = append(tags, types.KeyValueType{ + Key: att.Key, + Value: boolVal, + Type: "boolean", + }) + continue + } + + if att.Value.IntValue != "" { + intVal, err := strconv.Atoi(att.Value.IntValue) + if err != nil { + continue + } + tags = append(tags, types.KeyValueType{ + Key: att.Key, + Value: int64(intVal), + Type: "int64", + }) + continue + } + + if att.Value.DoubleValue != "" { + floatVal, err := strconv.ParseFloat(att.Value.DoubleValue, 64) + if err != nil { + continue + } + tags = append(tags, types.KeyValueType{ + Key: att.Key, + Value: floatVal, + Type: "float64", + }) + continue + } + + if len(att.Value.ArrayValue.Values) > 0 { + tags = append(tags, types.KeyValueType{ + Key: att.Key, + Value: att.Value.ArrayValue.Values, + }) + continue + } + + if len(att.Value.KvListValue.Values) > 0 { + tags = append(tags, types.KeyValueType{ + Key: att.Key, + Value: att.Value.KvListValue.Values, + }) + continue + } + + if att.Value.BytesValue != "" { + tags = append(tags, types.KeyValueType{ + Key: att.Key, + Value: att.Value.BytesValue, + Type: "bytes", + }) + continue + } + } + return tags +} + +func getAttribute(attributes []types.GrpcKeyValue, attName string) types.GrpcAnyValue { + var attValue types.GrpcAnyValue + for _, att := range attributes { + if att.Key == attName { + return att.Value + } + } + + return attValue +} + +func processSpanKind(kind int64) string { + switch kind { + case 0: + return "unspecified" + case 1: + return "internal" + case 2: + return "server" + case 3: + return "client" + case 4: + return "producer" + case 5: + return "consumer" + default: + return "unspecified" + } +} + +// This is to help ensure backwards compatibility with the current non OTLP based Jager trace format +// a few fields are different between TraceLogs and GrpcSpanEvents +func convertGrpcEventsToLogs(events []types.GrpcSpanEvent) []types.TraceLog { + logs := []types.TraceLog{} + + for _, event := range events { + timestamp, err := strconv.Atoi(event.TimeUnixNano) + if err == nil { + timestamp = timestamp / 1000 // converting from nanoseconds to milliseconds + } + log := types.TraceLog{ + Name: event.Name, + Timestamp: int64(timestamp), + Fields: processAttributes(event.Attributes), + } + logs = append(logs, log) + } + + return logs +} + +// this is to help ensure backwards compatibility between references and links with the current non OTLP based Jaeger trace format +// There is no concept of RefType in the new OTLP based SpanLink, so we are only converting the SpanID and TraceID +func convertGrpcLinkToReference(links []types.GrpcSpanLink) []types.TraceSpanReference { + references := []types.TraceSpanReference{} + + for _, ref := range links { + references = append(references, types.TraceSpanReference{ + TraceID: ref.TraceID, + SpanID: ref.SpanID, + }) + } + + return references +} diff --git a/pkg/tsdb/jaeger/utils/grpc_utils_test.go b/pkg/tsdb/jaeger/utils/grpc_utils_test.go new file mode 100644 index 00000000000..4c2b4294467 --- /dev/null +++ b/pkg/tsdb/jaeger/utils/grpc_utils_test.go @@ -0,0 +1,826 @@ +package utils + +import ( + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/tsdb/jaeger/types" + "github.com/stretchr/testify/assert" +) + +func TestTransformGrpcSearchResponse(t *testing.T) { + t.Run("empty_response", func(t *testing.T) { + frame := TransformGrpcSearchResponse(types.GrpcTracesResult{}, "test-uid", "test-name", 0) + experimental.CheckGoldenJSONFrame(t, "../testdata", "search_empty_response.golden", frame, false) + }) + + t.Run("single_trace", func(t *testing.T) { + response := types.GrpcTracesResult{ + ResourceSpans: []types.GrpcResourceSpans{ + { + Resource: types.GrpcResource{ + Attributes: []types.GrpcKeyValue{ + { + Key: "service.name", + Value: types.GrpcAnyValue{ + StringValue: "test-service", + }, + }, + }, + }, + ScopeSpans: []types.GrpcScopeSpans{ + { + Spans: []types.GrpcSpan{ + { + TraceID: "test-trace-id", + Name: "test-operation", + StartTimeUnixNano: "1605873894680409000", + EndTimeUnixNano: "1605873894681409000", + }, + }, + }, + }, + SchemaURL: "someschemaurl.com", + }, + }, + } + + frame := TransformGrpcSearchResponse(response, "test-uid", "test-name", 0) + experimental.CheckGoldenJSONFrame(t, "../testdata", "search_single_response.golden", frame, false) + }) + + t.Run("multiple_traces", func(t *testing.T) { + response := types.GrpcTracesResult{ + ResourceSpans: []types.GrpcResourceSpans{ + { + Resource: types.GrpcResource{ + Attributes: []types.GrpcKeyValue{ + { + Key: "service.name", + Value: types.GrpcAnyValue{ + StringValue: "service-1", + }, + }, + }, + }, + ScopeSpans: []types.GrpcScopeSpans{ + { + Spans: []types.GrpcSpan{ + { + TraceID: "trace-1", + Name: "op1", + StartTimeUnixNano: "1605873894680409000", + EndTimeUnixNano: "1605873894681409000", + }, + }, + }, + }, + SchemaURL: "someschemaurl.com", + }, + { + Resource: types.GrpcResource{ + Attributes: []types.GrpcKeyValue{ + { + Key: "service.name", + Value: types.GrpcAnyValue{ + StringValue: "service-2", + }, + }, + }, + }, + ScopeSpans: []types.GrpcScopeSpans{ + { + Spans: []types.GrpcSpan{ + { + TraceID: "trace-2", + Name: "op2", + StartTimeUnixNano: "1605873894680409000", + EndTimeUnixNano: "1605873894682409000", + }, + }, + }, + }, + SchemaURL: "someschemaurl.com", + }, + }, + } + frame := TransformGrpcSearchResponse(response, "test-uid", "test-name", 0) + experimental.CheckGoldenJSONFrame(t, "../testdata", "search_multiple_response.golden", frame, false) + }) +} +func TestGetAttributes(t *testing.T) { + testAttributes := []types.GrpcKeyValue{ + { + Key: "some-key1", + Value: types.GrpcAnyValue{ + StringValue: "some-stringValue1", + }, + }, + { + Key: "some-key2", + Value: types.GrpcAnyValue{ + BoolValue: "true", + }, + }, + { + Key: "some-key3", + Value: types.GrpcAnyValue{ + IntValue: "0", + }, + }, + { + Key: "some-key4", + Value: types.GrpcAnyValue{ + DoubleValue: "0", + }, + }, + { + Key: "some-key5", + Value: types.GrpcAnyValue{ + ArrayValue: types.GrpcArrayValue{ + Values: []types.GrpcAnyValue{}, + }, + }, + }, + { + Key: "some-key6", + Value: types.GrpcAnyValue{ + KvListValue: types.KeyValueList{ + Values: []types.GrpcKeyValue{}, + }, + }, + }, + { + Key: "some-key7", + Value: types.GrpcAnyValue{ + BytesValue: "somebytesvalue", + }, + }, + } + t.Run("handles StringValue", func(t *testing.T) { + actual := getAttribute(testAttributes, "some-key1") + assert.Equal(t, types.GrpcAnyValue{ + StringValue: "some-stringValue1", + }, actual) + }) + t.Run("handles BoolValue", func(t *testing.T) { + actual := getAttribute(testAttributes, "some-key2") + assert.Equal(t, types.GrpcAnyValue{ + BoolValue: "true", + }, actual) + }) + t.Run("handles IntValue", func(t *testing.T) { + actual := getAttribute(testAttributes, "some-key3") + assert.Equal(t, types.GrpcAnyValue{ + IntValue: "0", + }, actual) + }) + t.Run("handles DoubleValue", func(t *testing.T) { + actual := getAttribute(testAttributes, "some-key4") + assert.Equal(t, types.GrpcAnyValue{ + DoubleValue: "0", + }, actual) + }) + t.Run("handles ArrayValue", func(t *testing.T) { + actual := getAttribute(testAttributes, "some-key5") + assert.Equal(t, types.GrpcAnyValue{ + ArrayValue: types.GrpcArrayValue{ + Values: []types.GrpcAnyValue{}, + }, + }, actual) + }) + t.Run("handles KvListValue", func(t *testing.T) { + actual := getAttribute(testAttributes, "some-key6") + assert.Equal(t, types.GrpcAnyValue{ + KvListValue: types.KeyValueList{ + Values: []types.GrpcKeyValue{}, + }, + }, actual) + }) + t.Run("handles BytesValue", func(t *testing.T) { + actual := getAttribute(testAttributes, "some-key7") + assert.Equal(t, types.GrpcAnyValue{ + BytesValue: "somebytesvalue", + }, actual) + }) + t.Run("handles non-existent value", func(t *testing.T) { + actual := getAttribute(testAttributes, "some-key8") + assert.Equal(t, types.GrpcAnyValue{}, actual) + }) +} + +func TestTransformGrpcTraceResponse(t *testing.T) { + t.Run("simple_trace", func(t *testing.T) { + trace := []types.GrpcResourceSpans{ + { + Resource: types.GrpcResource{ + Attributes: []types.GrpcKeyValue{ + { + Key: "service.name", + Value: types.GrpcAnyValue{ + StringValue: "tempo-querier", + }, + }, + { + Key: "cluster", + Value: types.GrpcAnyValue{ + StringValue: "ops-tools1", + }, + }, + { + Key: "container", + Value: types.GrpcAnyValue{ + StringValue: "tempo-query", + }, + }, + }, + }, + ScopeSpans: []types.GrpcScopeSpans{ + { + Scope: types.GrpcInstrumentationScope{ + Name: "some_scope1", + Version: "0.0.39", + }, + Spans: []types.GrpcSpan{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + ParentSpanID: "", + Name: "HTTP GET - api_traces_traceid", + Attributes: []types.GrpcKeyValue{ + { + Key: "sampler.type", + Value: types.GrpcAnyValue{ + StringValue: "probabilistic", + }, + }, + { + Key: "sampler.param", + Value: types.GrpcAnyValue{ + DoubleValue: "100.00", + }, + }, + }, + StartTimeUnixNano: "1605873894680409000", + EndTimeUnixNano: "1605873895729550000", + }, + { + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + ParentSpanID: "3fa414edcef6ad90", + Name: "HTTP GET - api_traces_traceid", + Attributes: []types.GrpcKeyValue{ + { + Key: "component", + Value: types.GrpcAnyValue{ + StringValue: "gRPC", + }, + }, + { + Key: "span.kind", + Value: types.GrpcAnyValue{ + DoubleValue: "client", + }, + }, + }, + StartTimeUnixNano: "1605873894680587000", + EndTimeUnixNano: "1605873894682434000", + }, + }, + }, + }, + }, + } + frame := TransformGrpcTraceResponse(trace, "test") + experimental.CheckGoldenJSONFrame(t, "../testdata", "simple_trace_grpc.golden", frame, false) + }) + + t.Run("complex_trace", func(t *testing.T) { + trace := []types.GrpcResourceSpans{ + { + Resource: types.GrpcResource{ + Attributes: []types.GrpcKeyValue{ + { + Key: "service.name", + Value: types.GrpcAnyValue{ + StringValue: "tempo-querier", + }, + }, + { + Key: "cluster", + Value: types.GrpcAnyValue{ + StringValue: "ops-tools1", + }, + }, + { + Key: "container", + Value: types.GrpcAnyValue{ + StringValue: "tempo-storage", + }, + }, + { + Key: "version", + Value: types.GrpcAnyValue{ + StringValue: "2.0.1", + }, + }, + }, + }, + ScopeSpans: []types.GrpcScopeSpans{ + { + Spans: []types.GrpcSpan{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + Name: "HTTP GET - api_traces_traceid", + Links: []types.GrpcSpanLink{}, + StartTimeUnixNano: "1605873894680409000", + EndTimeUnixNano: "1605873895729550000", + Attributes: []types.GrpcKeyValue{ + { + Key: "sampler.type", + Value: types.GrpcAnyValue{ + StringValue: "probabilistic", + }, + }, + { + Key: "sampler.param", + Value: types.GrpcAnyValue{ + DoubleValue: "1", + }, + }, + { + Key: "error", + Value: types.GrpcAnyValue{ + BoolValue: "true", + }, + }, + { + Key: "http.status_code", + Value: types.GrpcAnyValue{ + IntValue: "500", + }, + }, + }, + Events: []types.GrpcSpanEvent{ + { + TimeUnixNano: "1605873894681000000", + Attributes: []types.GrpcKeyValue{ + { + Key: "event", + Value: types.GrpcAnyValue{ + StringValue: "error", + }, + }, + { + Key: "message", + Value: types.GrpcAnyValue{ + StringValue: "Internal server error", + }, + }, + }, + }, + }, + }, + { + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + Name: "/tempopb.Querier/FindTraceByID", + Links: []types.GrpcSpanLink{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + }, + }, + StartTimeUnixNano: "1605873894680587000", + EndTimeUnixNano: "1605873894682434000", + Attributes: []types.GrpcKeyValue{ + { + Key: "component", + Value: types.GrpcAnyValue{ + StringValue: "gRPC", + }, + }, + { + Key: "span.kind", + Value: types.GrpcAnyValue{ + StringValue: "client", + }, + }, + { + Key: "error", + Value: types.GrpcAnyValue{ + BoolValue: "true", + }, + }, + { + Key: "grpc.status_code", + Value: types.GrpcAnyValue{ + IntValue: "13", + }, + }, + }, + Events: []types.GrpcSpanEvent{ + { + TimeUnixNano: "1605873894680700000", + Attributes: []types.GrpcKeyValue{ + { + Key: "event", + Value: types.GrpcAnyValue{ + StringValue: "error", + }, + }, + { + Key: "message", + Value: types.GrpcAnyValue{ + StringValue: "gRPC error: INTERNAL", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Resource: types.GrpcResource{ + Attributes: []types.GrpcKeyValue{ + { + Key: "service.name", + Value: types.GrpcAnyValue{ + StringValue: "tempo-storage", + }, + }, + { + Key: "cluster", + Value: types.GrpcAnyValue{ + StringValue: "ops-tools1", + }, + }, + { + Key: "container", + Value: types.GrpcAnyValue{ + StringValue: "tempo-storage", + }, + }, + { + Key: "version", + Value: types.GrpcAnyValue{ + StringValue: "2.0.1", + }, + }, + }, + }, + ScopeSpans: []types.GrpcScopeSpans{ + { + Spans: []types.GrpcSpan{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "1a2b3c4d5e6f7g8h", + Name: "db.query", + Links: []types.GrpcSpanLink{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + }, + }, + StartTimeUnixNano: "1605873894680800000", + EndTimeUnixNano: "1605873894681300000", + Attributes: []types.GrpcKeyValue{ + { + Key: "db.type", + Value: types.GrpcAnyValue{ + StringValue: "postgresql", + }, + }, + { + Key: "db.statement", + Value: types.GrpcAnyValue{ + StringValue: "SELECT * FROM traces WHERE id = $1", + }, + }, + { + Key: "error", + Value: types.GrpcAnyValue{ + BoolValue: "true", + }, + }, + }, + Events: []types.GrpcSpanEvent{ + { + TimeUnixNano: "1605873894681000000", + Attributes: []types.GrpcKeyValue{ + { + Key: "event", + Value: types.GrpcAnyValue{ + StringValue: "error", + }, + }, + { + Key: "message", + Value: types.GrpcAnyValue{ + StringValue: "Database connection timeout", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + frame := TransformGrpcTraceResponse(trace, "test") + experimental.CheckGoldenJSONFrame(t, "../testdata", "complex_trace_grpc.golden", frame, false) + }) +} + +func TestProcessSpanKind(t *testing.T) { + t.Run("converts unspecified span kind", func(t *testing.T) { + actual := processSpanKind(0) + assert.Equal(t, "unspecified", actual) + }) + + t.Run("converts internal span kind", func(t *testing.T) { + actual := processSpanKind(1) + assert.Equal(t, "internal", actual) + }) + + t.Run("converts server span kind", func(t *testing.T) { + actual := processSpanKind(2) + assert.Equal(t, "server", actual) + }) + + t.Run("converts client span kind", func(t *testing.T) { + actual := processSpanKind(3) + assert.Equal(t, "client", actual) + }) + + t.Run("converts producer span kind", func(t *testing.T) { + actual := processSpanKind(4) + assert.Equal(t, "producer", actual) + }) + + t.Run("converts consumer span kind", func(t *testing.T) { + actual := processSpanKind(5) + assert.Equal(t, "consumer", actual) + }) + + t.Run("converts unsupported span kind", func(t *testing.T) { + actual := processSpanKind(10) + assert.Equal(t, "unspecified", actual) + }) +} + +func TestProcessAttributes(t *testing.T) { + t.Run("processes empty attributes", func(t *testing.T) { + actual := processAttributes([]types.GrpcKeyValue{}) + assert.Equal(t, []types.KeyValueType{}, actual) + }) + t.Run("processes string attribute types", func(t *testing.T) { + attributes := []types.GrpcKeyValue{ + { + Key: "key1", + Value: types.GrpcAnyValue{ + StringValue: "value1", + }, + }, + } + expected := []types.KeyValueType{ + { + Key: "key1", + Value: "value1", + Type: "string", + }, + } + actual := processAttributes(attributes) + assert.Equal(t, expected, actual) + }) + + t.Run("processes bool attribute types", func(t *testing.T) { + attributes := []types.GrpcKeyValue{ + { + Key: "key1", + Value: types.GrpcAnyValue{ + BoolValue: "true", + }, + }, + } + expected := []types.KeyValueType{ + { + Key: "key1", + Value: true, + Type: "boolean", + }, + } + actual := processAttributes(attributes) + assert.Equal(t, expected, actual) + }) + + t.Run("processes int attribute types", func(t *testing.T) { + attributes := []types.GrpcKeyValue{ + { + Key: "key1", + Value: types.GrpcAnyValue{ + IntValue: "10", + }, + }, + } + expected := []types.KeyValueType{ + { + Key: "key1", + Value: int64(10), + Type: "int64", + }, + } + actual := processAttributes(attributes) + assert.Equal(t, expected, actual) + }) + + t.Run("processes double attribute types", func(t *testing.T) { + attributes := []types.GrpcKeyValue{ + { + Key: "key1", + Value: types.GrpcAnyValue{ + DoubleValue: "100.50", + }, + }, + } + expected := []types.KeyValueType{ + { + Key: "key1", + Value: float64(100.50), + Type: "float64", + }, + } + actual := processAttributes(attributes) + assert.Equal(t, expected, actual) + }) + + t.Run("processes arrayvalue attribute types", func(t *testing.T) { + attributes := []types.GrpcKeyValue{ + { + Key: "key1", + Value: types.GrpcAnyValue{ + ArrayValue: types.GrpcArrayValue{ + Values: []types.GrpcAnyValue{ + { + StringValue: "value1", + }, + }, + }, + }, + }, + } + expected := []types.KeyValueType{ + { + Key: "key1", + Value: []types.GrpcAnyValue{ + { + StringValue: "value1", + }, + }, + }, + } + actual := processAttributes(attributes) + assert.Equal(t, expected, actual) + }) + + t.Run("processes kvlistvalue attribute types", func(t *testing.T) { + attributes := []types.GrpcKeyValue{ + { + Key: "key1", + Value: types.GrpcAnyValue{ + KvListValue: types.KeyValueList{ + Values: []types.GrpcKeyValue{ + { + Key: "key2", + Value: types.GrpcAnyValue{ + StringValue: "value2", + }, + }, + }, + }, + }, + }, + } + expected := []types.KeyValueType{ + { + Key: "key1", + Value: []types.GrpcKeyValue{ + { + Key: "key2", + Value: types.GrpcAnyValue{ + StringValue: "value2", + }, + }, + }, + }, + } + actual := processAttributes(attributes) + assert.Equal(t, expected, actual) + }) + + t.Run("processes bytes attribute types", func(t *testing.T) { + attributes := []types.GrpcKeyValue{ + { + Key: "key1", + Value: types.GrpcAnyValue{ + BytesValue: "bytesvalue1", + }, + }, + } + expected := []types.KeyValueType{ + { + Key: "key1", + Value: "bytesvalue1", + Type: "bytes", + }, + } + actual := processAttributes(attributes) + assert.Equal(t, expected, actual) + }) +} + +func TestConvertGrpcEventsToLogs(t *testing.T) { + t.Run("converts events with timestamp and attributes", func(t *testing.T) { + events := []types.GrpcSpanEvent{ + { + TimeUnixNano: "2000", + Name: "error", + Attributes: []types.GrpcKeyValue{ + { + Key: "event", + Value: types.GrpcAnyValue{ + StringValue: "error", + }, + }, + }, + }, + } + + logs := convertGrpcEventsToLogs(events) + + expected := []types.TraceLog{ + { + Name: "error", + Timestamp: int64(2), + Fields: []types.KeyValueType{ + { + Key: "event", + Value: "error", + Type: "string", + }, + }, + }, + } + + assert.Equal(t, expected, logs) + }) + + t.Run("returns zero timestamp when parsing fails", func(t *testing.T) { + events := []types.GrpcSpanEvent{ + { + TimeUnixNano: "invalid", + Name: "log-without-timestamp", + }, + } + + logs := convertGrpcEventsToLogs(events) + assert.Len(t, logs, 1) + assert.Equal(t, int64(0), logs[0].Timestamp) + assert.Equal(t, "log-without-timestamp", logs[0].Name) + assert.Empty(t, logs[0].Fields) + }) +} + +func TestConvertGrpcLinkToReference(t *testing.T) { + t.Run("converts links to references", func(t *testing.T) { + links := []types.GrpcSpanLink{ + { + TraceID: "trace-id", + SpanID: "span-id", + }, + } + + references := convertGrpcLinkToReference(links) + + expected := []types.TraceSpanReference{ + { + TraceID: "trace-id", + SpanID: "span-id", + }, + } + + assert.Equal(t, expected, references) + }) + + t.Run("returns empty slice for no links", func(t *testing.T) { + references := convertGrpcLinkToReference(nil) + assert.Empty(t, references) + }) +} diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 92fe36c3d26..2ce2ecd8781 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -15,22 +15,19 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/httpclient" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" ) -var logger = log.New("tsdb.opentsdb") +var logger = backend.NewLoggerWith("tsdb.opentsdb") type Service struct { im instancemgmt.InstanceManager } -func ProvideService(httpClientProvider httpclient.Provider) *Service { +func ProvideService(httpClientProvider *httpclient.Provider) *Service { return &Service{ im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)), } @@ -52,7 +49,22 @@ type JSONData struct { LookupLimit int32 `json:"lookupLimit"` } -func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { +type QueryModel struct { + Metric string `json:"metric"` + Aggregator string `json:"aggregator"` + DownsampleInterval string `json:"downsampleInterval"` + DownsampleAggregator string `json:"downsampleAggregator"` + DownsampleFillPolicy string `json:"downsampleFillPolicy"` + DisableDownsampling bool `json:"disableDownsampling"` + Filters []any `json:"filters"` + Tags map[string]interface{} `json:"tags"` + ShouldComputeRate bool `json:"shouldComputeRate"` + IsCounter bool `json:"isCounter"` + CounterMax float64 `json:"counterMax"` + CounterResetValue float64 `json:"counterResetValue"` +} + +func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { opts, err := settings.HTTPClientOptions(ctx) if err != nil { @@ -102,10 +114,6 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) }, } - if setting.Env == setting.Dev { - logger.Debug("OpenTsdb request", "refId", query.RefID, "params", tsdbQuery) - } - httpReq, err := s.createRequest(ctx, logger, dsInfo, tsdbQuery) if err != nil { return nil, err @@ -274,47 +282,44 @@ func (s *Service) parseResponse(logger log.Logger, res *http.Response, refID str func (s *Service) buildMetric(query backend.DataQuery) map[string]any { metric := make(map[string]any) - model, err := simplejson.NewJson(query.JSON) - if err != nil { + var model QueryModel + if err := json.Unmarshal(query.JSON, &model); err != nil { return nil } // Setting metric and aggregator - metric["metric"] = model.Get("metric").MustString() - metric["aggregator"] = model.Get("aggregator").MustString() + metric["metric"] = model.Metric + metric["aggregator"] = model.Aggregator // Setting downsampling options - disableDownsampling := model.Get("disableDownsampling").MustBool() - if !disableDownsampling { - downsampleInterval := model.Get("downsampleInterval").MustString() + if !model.DisableDownsampling { + downsampleInterval := model.DownsampleInterval if downsampleInterval == "" { downsampleInterval = "1m" // default value for blank } - downsample := downsampleInterval + "-" + model.Get("downsampleAggregator").MustString() - if model.Get("downsampleFillPolicy").MustString() != "none" { - metric["downsample"] = downsample + "-" + model.Get("downsampleFillPolicy").MustString() + downsample := downsampleInterval + "-" + model.DownsampleAggregator + if model.DownsampleFillPolicy != "none" { + metric["downsample"] = downsample + "-" + model.DownsampleFillPolicy } else { metric["downsample"] = downsample } } // Setting rate options - if model.Get("shouldComputeRate").MustBool() { + if model.ShouldComputeRate { metric["rate"] = true rateOptions := make(map[string]any) - rateOptions["counter"] = model.Get("isCounter").MustBool() + rateOptions["counter"] = model.IsCounter - counterMax, counterMaxCheck := model.CheckGet("counterMax") - if counterMaxCheck { - rateOptions["counterMax"] = counterMax.MustFloat64() + if model.CounterMax != 0 { + rateOptions["counterMax"] = model.CounterMax } - resetValue, resetValueCheck := model.CheckGet("counterResetValue") - if resetValueCheck { - rateOptions["resetValue"] = resetValue.MustFloat64() + if model.CounterResetValue != 0 { + rateOptions["resetValue"] = model.CounterResetValue } - if !counterMaxCheck && (!resetValueCheck || resetValue.MustFloat64() == 0) { + if model.CounterMax == 0 && (model.CounterResetValue == 0) { rateOptions["dropResets"] = true } @@ -322,15 +327,13 @@ func (s *Service) buildMetric(query backend.DataQuery) map[string]any { } // Setting tags - tags, tagsCheck := model.CheckGet("tags") - if tagsCheck && len(tags.MustMap()) > 0 { - metric["tags"] = tags.MustMap() + if len(model.Tags) > 0 { + metric["tags"] = model.Tags } // Setting filters - filters, filtersCheck := model.CheckGet("filters") - if filtersCheck && len(filters.MustArray()) > 0 { - metric["filters"] = filters.MustArray() + if len(model.Filters) > 0 { + metric["filters"] = model.Filters } return metric diff --git a/pkg/tsdb/opentsdb/opentsdb_test.go b/pkg/tsdb/opentsdb/opentsdb_test.go index 11a13d7ddd5..da1950e0aad 100644 --- a/pkg/tsdb/opentsdb/opentsdb_test.go +++ b/pkg/tsdb/opentsdb/opentsdb_test.go @@ -12,8 +12,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/tsdb/opentsdb/standalone/datasource.go b/pkg/tsdb/opentsdb/standalone/datasource.go new file mode 100644 index 00000000000..7ca315aabb5 --- /dev/null +++ b/pkg/tsdb/opentsdb/standalone/datasource.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + opentsdb "github.com/grafana/grafana/pkg/tsdb/opentsdb" +) + +var ( + _ backend.QueryDataHandler = (*Datasource)(nil) +) + +type Datasource struct { + Service *opentsdb.Service +} + +func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return &Datasource{ + Service: opentsdb.ProvideService(httpclient.NewProvider()), + }, nil +} + +func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + return d.Service.QueryData(ctx, req) +} diff --git a/pkg/tsdb/opentsdb/standalone/main.go b/pkg/tsdb/opentsdb/standalone/main.go new file mode 100644 index 00000000000..79d3a3cd518 --- /dev/null +++ b/pkg/tsdb/opentsdb/standalone/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "os" + + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" +) + +func main() { + if err := datasource.Manage("opentsdb", NewDatasource, datasource.ManageOpts{}); err != nil { + log.DefaultLogger.Error(err.Error()) + os.Exit(1) + } +} diff --git a/pkg/tsdb/parca/query.go b/pkg/tsdb/parca/query.go index 7c8d5931cb7..db259a4d56b 100644 --- a/pkg/tsdb/parca/query.go +++ b/pkg/tsdb/parca/query.go @@ -196,9 +196,7 @@ func arrowToNestedSetDataFrame(flamegraph *v1alpha1.FlamegraphArrow) (*data.Fram defer arrowReader.Release() arrowReader.Next() - //TODO: fix deprecation warning - //nolint:staticcheck - rec := arrowReader.Record() + rec := arrowReader.RecordBatch() fi, err := newFlamegraphIterator(rec) if err != nil { @@ -238,10 +236,7 @@ type flamegraphIterator struct { addressBuilder *bytes.Buffer } -// TODO: fix deprecation warning -// -//nolint:staticcheck -func newFlamegraphIterator(rec arrow.Record) (*flamegraphIterator, error) { +func newFlamegraphIterator(rec arrow.RecordBatch) (*flamegraphIterator, error) { schema := rec.Schema() columnChildren := rec.Column(schema.FieldIndices(FlamegraphFieldChildren)[0]).(*array.List) diff --git a/pkg/tsdb/parca/query_test.go b/pkg/tsdb/parca/query_test.go index 6a18993ba98..0ed26d7eab0 100644 --- a/pkg/tsdb/parca/query_test.go +++ b/pkg/tsdb/parca/query_test.go @@ -278,9 +278,7 @@ func flamegraphResponse() *connect.Response[v1alpha1.QueryResponse] { builderFlat.Append(columns.flat[i]) } - //TODO: fix deprecation warning - //nolint:staticcheck - record := array.NewRecord( + record := array.NewRecordBatch( arrow.NewSchema(fields, nil), []arrow.Array{ builderLocationAddress.NewArray(), diff --git a/pkg/util/testutil/context_test.go b/pkg/util/testutil/context_test.go index b841deaa7e6..4d7ecf670f6 100644 --- a/pkg/util/testutil/context_test.go +++ b/pkg/util/testutil/context_test.go @@ -7,14 +7,12 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "go.opencensus.io/stats/view" "go.uber.org/goleak" "github.com/grafana/grafana/pkg/util/testutil/mocks" ) func TestMain(m *testing.M) { - view.Stop() // make sure we don't leak goroutines after tests in this package have // finished, which means we haven't leaked contexts either goleak.VerifyTestMain(m) diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 46c47a02e82..a7dfb2004ec 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -406,6 +406,14 @@ "name": "teamId", "in": "path", "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SetTeamRolesCommand" + } } ], "responses": { @@ -2577,6 +2585,9 @@ }, "AddTeamMemberCommand": { "type": "object", + "required": [ + "userId" + ], "properties": { "userId": { "type": "integer", @@ -2975,6 +2986,10 @@ "description": "Name of annotation.", "type": "string" }, + "placement": { + "description": "Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.", + "type": "string" + }, "target": { "$ref": "#/definitions/AnnotationTarget" }, @@ -3969,6 +3984,9 @@ }, "CreateTeamCommand": { "type": "object", + "required": [ + "name" + ], "properties": { "email": { "type": "string" @@ -4323,6 +4341,20 @@ } } }, + "DashboardVersionResponseMeta": { + "type": "object", + "properties": { + "continueToken": { + "type": "string" + }, + "versions": { + "type": "array", + "items": { + "$ref": "#/definitions/DashboardVersionMeta" + } + } + } + }, "DataLink": { "description": "DataLink define what", "type": "object", @@ -6144,8 +6176,8 @@ } }, "Permission": { + "description": "Permission is the model for access control permissions", "type": "object", - "title": "Permission is the model for access control permissions.", "properties": { "action": { "type": "string" @@ -7162,6 +7194,16 @@ }, "RoleDTO": { "type": "object", + "required": [ + "version", + "uid", + "name", + "displayName", + "description", + "group", + "updated", + "created" + ], "properties": { "created": { "type": "string", @@ -7674,6 +7716,20 @@ } } }, + "SetTeamRolesCommand": { + "type": "object", + "properties": { + "includeHidden": { + "type": "boolean" + }, + "roleUids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "SetUserRolesCommand": { "type": "object", "properties": { @@ -7847,6 +7903,14 @@ }, "TeamDTO": { "type": "object", + "required": [ + "id", + "uid", + "orgId", + "name", + "isProvisioned", + "memberCount" + ], "properties": { "accessControl": { "type": "object", @@ -7864,6 +7928,7 @@ "type": "string" }, "id": { + "description": "@deprecated Use UID instead", "type": "integer", "format": "int64" }, @@ -7979,6 +8044,9 @@ "teamUID": { "type": "string" }, + "uid": { + "type": "string" + }, "userId": { "type": "integer", "format": "int64" @@ -9420,10 +9488,7 @@ "dashboardVersionsResponse": { "description": "", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/DashboardVersionMeta" - } + "$ref": "#/definitions/DashboardVersionResponseMeta" } }, "deleteCorrelationResponse": { diff --git a/public/api-merged.json b/public/api-merged.json index f6ca6d73502..a168fed65c0 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -406,6 +406,14 @@ "name": "teamId", "in": "path", "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SetTeamRolesCommand" + } } ], "responses": { @@ -3676,7 +3684,7 @@ "tags": [ "dashboards" ], - "summary": "Interpolate dashboard. This is an experimental endpoint under dashboardLibrary FF and is subject to change.", + "summary": "Interpolate dashboard. This is an experimental endpoint under dashboardLibrary or suggestedDashboards feature flags and is subject to change.", "operationId": "interpolateDashboard", "responses": { "200": { @@ -9931,6 +9939,17 @@ "description": "If set it will return results where the query value is contained in the name field. Query values with spaces need to be URL encoded.", "name": "query", "in": "query" + }, + { + "type": "boolean", + "default": false, + "name": "accesscontrol", + "in": "query" + }, + { + "type": "string", + "name": "sort", + "in": "query" } ], "responses": { @@ -12750,6 +12769,9 @@ }, "AddTeamMemberCommand": { "type": "object", + "required": [ + "userId" + ], "properties": { "userId": { "type": "integer", @@ -13735,6 +13757,10 @@ "description": "Name of annotation.", "type": "string" }, + "placement": { + "description": "Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.", + "type": "string" + }, "target": { "$ref": "#/definitions/AnnotationTarget" }, @@ -14942,6 +14968,9 @@ }, "CreateTeamCommand": { "type": "object", + "required": [ + "name" + ], "properties": { "email": { "type": "string" @@ -18647,8 +18676,8 @@ } }, "Permission": { + "description": "Permission is the model for access control permissions", "type": "object", - "title": "Permission is the model for access control permissions.", "properties": { "action": { "type": "string" @@ -20558,6 +20587,16 @@ }, "RoleDTO": { "type": "object", + "required": [ + "version", + "uid", + "name", + "displayName", + "description", + "group", + "updated", + "created" + ], "properties": { "created": { "type": "string", @@ -21445,6 +21484,20 @@ } } }, + "SetTeamRolesCommand": { + "type": "object", + "properties": { + "includeHidden": { + "type": "boolean" + }, + "roleUids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "SetUserRolesCommand": { "type": "object", "properties": { @@ -21881,6 +21934,14 @@ }, "TeamDTO": { "type": "object", + "required": [ + "id", + "uid", + "orgId", + "name", + "isProvisioned", + "memberCount" + ], "properties": { "accessControl": { "type": "object", @@ -21898,6 +21959,7 @@ "type": "string" }, "id": { + "description": "@deprecated Use UID instead", "type": "integer", "format": "int64" }, diff --git a/public/app/api/clients/folder/v1beta1/hooks.ts b/public/app/api/clients/folder/v1beta1/hooks.ts index 3ee45cde1ae..1793818bdd6 100644 --- a/public/app/api/clients/folder/v1beta1/hooks.ts +++ b/public/app/api/clients/folder/v1beta1/hooks.ts @@ -4,6 +4,7 @@ import { useEffect, useMemo } from 'react'; import { AppEvents } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config, getAppEvents } from '@grafana/runtime'; +import { DisplayList, iamAPIv0alpha1, useLazyGetDisplayMappingQuery } from 'app/api/clients/iam/v0alpha1'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useDeleteFolderMutation as useDeleteFolderMutationLegacy, @@ -17,9 +18,11 @@ import { MoveFoldersArgs, DeleteFoldersArgs, MoveFolderArgs, + browseDashboardsAPI, } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { DashboardTreeSelection } from 'app/features/browse-dashboards/types'; import { FolderDTO, NewFolder } from 'app/types/folders'; +import { dispatch } from 'app/types/store'; import kbn from '../../../../core/utils/kbn'; import { @@ -36,12 +39,12 @@ import { refetchChildren, refreshParents } from '../../../../features/browse-das import { GENERAL_FOLDER_UID } from '../../../../features/search/constants'; import { deletedDashboardsCache } from '../../../../features/search/service/deletedDashboardsCache'; import { useDispatch } from '../../../../types/store'; -import { useLazyGetDisplayMappingQuery } from '../../iam/v0alpha1'; import { isProvisionedFolderCheck } from './utils'; import { rootFolder, sharedWithMeFolder } from './virtualFolders'; import { + folderAPIv1beta1, useGetFolderQuery, useGetFolderParentsQuery, useDeleteFolderMutation, @@ -52,6 +55,7 @@ import { useReplaceFolderMutation, ReplaceFolderApiArg, useGetAffectedItemsQuery, + FolderInfo, } from './index'; function getFolderUrl(uid: string, title: string): string { @@ -62,6 +66,91 @@ function getFolderUrl(uid: string, title: string): string { return `${config.appSubUrl}/dashboards/f/${uid}/${slug}`; } +const combineFolderResponses = ( + folder: Folder, + legacyFolder: FolderDTO, + parents: FolderInfo[], + userDisplay?: DisplayList +) => { + const updatedBy = folder.metadata.annotations?.[AnnoKeyUpdatedBy]; + const createdBy = folder.metadata.annotations?.[AnnoKeyCreatedBy]; + + const newData: FolderDTO = { + canAdmin: legacyFolder.canAdmin, + canDelete: legacyFolder.canDelete, + canEdit: legacyFolder.canEdit, + canSave: legacyFolder.canSave, + accessControl: legacyFolder.accessControl, + createdBy: (createdBy && userDisplay?.display[userDisplay?.keys.indexOf(createdBy)]?.displayName) || 'Anonymous', + updatedBy: (updatedBy && userDisplay?.display[userDisplay?.keys.indexOf(updatedBy)]?.displayName) || 'Anonymous', + ...appPlatformFolderToLegacyFolder(folder), + }; + + if (parents.length) { + newData.parents = parents + .filter((i) => i.name !== folder.metadata.name) + .map(({ name, title }) => ({ + title: title, + uid: name, + // No idea how to make slug, on the server it uses a go lib: https://github.com/grafana/grafana/blob/aac66e91198004bc044754105e18bfff8fbfd383/pkg/infra/slugify/slugify.go#L56 + // Don't think slug is needed for the URL to work though + url: getFolderUrl(name, title), + })); + } + + return newData; +}; + +export async function getFolderByUidFacade(uid: string): Promise { + const isVirtualFolder = uid && [GENERAL_FOLDER_UID, config.sharedWithMeFolderUID].includes(uid); + // We need the legacy API call regardless, for now + const legacyApiCall = dispatch(browseDashboardsAPI.endpoints.getFolder.initiate(uid)); + + const shouldUseAppPlatformAPI = Boolean(config.featureToggles.foldersAppPlatformAPI); + if (shouldUseAppPlatformAPI) { + let virtualFolderResponse; + if (isVirtualFolder) { + virtualFolderResponse = GENERAL_FOLDER_UID === uid ? rootFolder : sharedWithMeFolder; + } + + const responses = await Promise.all([ + // We still need to call legacy endpoints for access control metadata + legacyApiCall, + isVirtualFolder + ? Promise.resolve({ data: virtualFolderResponse }) + : dispatch(folderAPIv1beta1.endpoints.getFolder.initiate({ name: uid })), + dispatch(folderAPIv1beta1.endpoints.getFolderParents.initiate({ name: uid })), + ]); + + const [legacyFolderResponse, folderResponse, parentsResponse] = responses; + + if (!folderResponse?.data || !legacyFolderResponse?.data || !parentsResponse?.data) { + throw new Error('One of the folder responses is undefined'); + } + + const userKeys = getUserKeys(folderResponse.data); + let userResponse; + if (userKeys.length) { + userResponse = await dispatch(iamAPIv0alpha1.endpoints.getDisplayMapping.initiate({ key: userKeys })); + } + + return combineFolderResponses( + folderResponse.data, + legacyFolderResponse.data, + parentsResponse.data.items, + userResponse?.data + ); + } + + const legacyFolderResponse = await legacyApiCall; + + if (legacyFolderResponse.error || !legacyFolderResponse.data) { + throw legacyFolderResponse.error || new Error('Legacy folder response is undefined'); + } + + return legacyFolderResponse.data; +} + /** * A proxy function that uses either legacy folder client or the new app platform APIs to get the data in the same * format of a FolderDTO object. As the schema isn't the same, using the app platform needs multiple different calls @@ -84,12 +173,12 @@ export function useGetFolderQueryFacade(uid?: string) { const [triggerGetUserDisplayMapping, resultUserDisplay] = useLazyGetDisplayMappingQuery(); const needsUserData = useMemo(() => { - const userKeys = getUserKeys(resultFolder); + const userKeys = getUserKeys(resultFolder.data); return !isVirtualFolder && Boolean(userKeys.length); }, [isVirtualFolder, resultFolder]); useEffect(() => { - const userKeys = getUserKeys(resultFolder); + const userKeys = getUserKeys(resultFolder.data); if (needsUserData && userKeys.length) { triggerGetUserDisplayMapping({ key: userKeys }, true); } @@ -125,39 +214,12 @@ export function useGetFolderQueryFacade(uid?: string) { legacyFolderResult.data && (needsUserData ? resultUserDisplay.data : true) ) { - const updatedBy = resultFolder.data.metadata.annotations?.[AnnoKeyUpdatedBy]; - const createdBy = resultFolder.data.metadata.annotations?.[AnnoKeyCreatedBy]; - - const parsed = appPlatformFolderToLegacyFolder(resultFolder.data); - - newData = { - canAdmin: legacyFolderResult.data.canAdmin, - canDelete: legacyFolderResult.data.canDelete, - canEdit: legacyFolderResult.data.canEdit, - canSave: legacyFolderResult.data.canSave, - accessControl: legacyFolderResult.data.accessControl, - - createdBy: - (createdBy && resultUserDisplay.data?.display[resultUserDisplay.data?.keys.indexOf(createdBy)]?.displayName) || - 'Anonymous', - - updatedBy: - (updatedBy && resultUserDisplay.data?.display[resultUserDisplay.data?.keys.indexOf(updatedBy)]?.displayName) || - 'Anonymous', - ...parsed, - }; - - if (resultParents.data.items?.length) { - newData.parents = resultParents.data.items - .filter((i) => i.name !== resultFolder.data!.metadata.name) - .map((i) => ({ - title: i.title, - uid: i.name, - // No idea how to make slug, on the server it uses a go lib: https://github.com/grafana/grafana/blob/aac66e91198004bc044754105e18bfff8fbfd383/pkg/infra/slugify/slugify.go#L56 - // Don't think slug is needed for the URL to work though - url: getFolderUrl(i.name, i.title), - })); - } + newData = combineFolderResponses( + resultFolder.data, + legacyFolderResult.data, + resultParents.data.items, + resultUserDisplay.data + ); } // Wrap the stitched data into single RTK query response type object so this looks like a single API call @@ -440,13 +502,10 @@ function combinedState( }; } -function getUserKeys(resultFolder: ReturnType): string[] { - return resultFolder.data - ? [ - resultFolder.data.metadata.annotations?.[AnnoKeyUpdatedBy], - resultFolder.data.metadata.annotations?.[AnnoKeyCreatedBy], - ].filter((v) => v !== undefined) - : []; +function getUserKeys(folder?: Folder): string[] { + return [folder?.metadata.annotations?.[AnnoKeyUpdatedBy], folder?.metadata.annotations?.[AnnoKeyCreatedBy]].filter( + (v) => v !== undefined + ); } const appPlatformFolderToLegacyFolder = ( diff --git a/public/app/api/clients/folder/v1beta1/index.ts b/public/app/api/clients/folder/v1beta1/index.ts index d701554cce7..13d2fa05791 100644 --- a/public/app/api/clients/folder/v1beta1/index.ts +++ b/public/app/api/clients/folder/v1beta1/index.ts @@ -80,9 +80,4 @@ export const { } = folderAPIv1beta1; // eslint-disable-next-line no-barrel-files/no-barrel-files -export { - type Folder, - type FolderList, - type CreateFolderApiArg, - type ReplaceFolderApiArg, -} from '@grafana/api-clients/rtkq/folder/v1beta1'; +export * from '@grafana/api-clients/rtkq/folder/v1beta1'; diff --git a/public/app/api/clients/iam/v0alpha1/index.ts b/public/app/api/clients/iam/v0alpha1/index.ts index 3f9e7b1fb6d..190f7ede868 100644 --- a/public/app/api/clients/iam/v0alpha1/index.ts +++ b/public/app/api/clients/iam/v0alpha1/index.ts @@ -3,3 +3,6 @@ import { generatedAPI } from '@grafana/api-clients/rtkq/iam/v0alpha1'; export const iamAPIv0alpha1 = generatedAPI.enhanceEndpoints({}); export const { useGetDisplayMappingQuery, useLazyGetDisplayMappingQuery } = iamAPIv0alpha1; + +// eslint-disable-next-line no-barrel-files/no-barrel-files +export type { DisplayList } from '@grafana/api-clients/rtkq/iam/v0alpha1'; diff --git a/public/app/api/clients/legacy/index.ts b/public/app/api/clients/legacy/index.ts new file mode 100644 index 00000000000..6a6abcf4054 --- /dev/null +++ b/public/app/api/clients/legacy/index.ts @@ -0,0 +1,6 @@ +import { generatedAPI } from '@grafana/api-clients/rtkq/legacy'; + +export const legacyAPI = generatedAPI.enhanceEndpoints({}); + +// eslint-disable-next-line no-barrel-files/no-barrel-files +export * from '@grafana/api-clients/rtkq/legacy'; diff --git a/public/app/api/clients/shorturl/v1alpha1/index.ts b/public/app/api/clients/shorturl/v1beta1/index.ts similarity index 75% rename from public/app/api/clients/shorturl/v1alpha1/index.ts rename to public/app/api/clients/shorturl/v1beta1/index.ts index 276fd01f3a8..ff238707b93 100644 --- a/public/app/api/clients/shorturl/v1alpha1/index.ts +++ b/public/app/api/clients/shorturl/v1beta1/index.ts @@ -1,6 +1,6 @@ -import { generatedAPI } from '@grafana/api-clients/rtkq/shorturl/v1alpha1'; +import { generatedAPI } from '@grafana/api-clients/rtkq/shorturl/v1beta1'; -export const shortURLAPIv1alpha1 = generatedAPI.enhanceEndpoints({ +export const shortURLAPIv1beta1 = generatedAPI.enhanceEndpoints({ endpoints: { createShortUrl: (endpointDefinition) => { const originalQuery = endpointDefinition.query; @@ -17,8 +17,7 @@ export const shortURLAPIv1alpha1 = generatedAPI.enhanceEndpoints({ const metadata = requestOptions.shortUrl.metadata; if (!metadata.name && !metadata.generateName) { // GenerateName lets the apiserver create a new uid for the name - // This wont be used, the backend will generate a random uid but cannot be blank or will fail. - metadata.generateName = 's-'; + metadata.generateName = 's'; // becomes a prefix } return originalQuery(requestOptions); }; diff --git a/public/app/app.ts b/public/app/app.ts index 76a77a46fd7..b95b60fac40 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -1,7 +1,6 @@ import 'symbol-observable'; import 'regenerator-runtime/runtime'; -import '@formatjs/intl-durationformat/polyfill'; import 'whatwg-fetch'; // fetch polyfill needed for PhantomJs rendering import 'file-saver'; import 'jquery'; diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index 568fb3fcf3e..bc132207698 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -45,8 +45,7 @@ export function AppChrome({ children }: Props) { ); const headerLevels = useChromeHeaderLevels(); - const headerHeight = headerLevels * getChromeHeaderLevelHeight(); - const styles = useStyles2(getStyles, headerHeight); + const styles = useStyles2(getStyles, headerLevels, getChromeHeaderLevelHeight()); const contentSizeStyles = useStyles2(getContentSizeStyles, extensionSidebarWidth); const dragStyles = useStyles2(getDragStyles); @@ -186,13 +185,13 @@ function useResponsiveDockedMegaMenu(chrome: AppChromeService) { }, [isLargeScreen, chrome, dockedMenuLocalStorageState]); } -const getStyles = (theme: GrafanaTheme2, headerHeight: number) => { +const getStyles = (theme: GrafanaTheme2, headerLevels: number, headerHeight: number) => { return { content: css({ label: 'page-content', display: 'flex', flexDirection: 'column', - paddingTop: headerHeight, + paddingTop: headerLevels * headerHeight, flexGrow: 1, height: 'auto', }), @@ -282,7 +281,7 @@ const getStyles = (theme: GrafanaTheme2, headerHeight: number) => { position: 'fixed !important' as 'fixed', top: headerHeight, bottom: 0, - zIndex: 2, + zIndex: theme.zIndex.navbarFixed + 1, right: 0, }), }; diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx index a3e56e4a3a8..e81c870fbda 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx @@ -107,7 +107,9 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi })} > {level === 0 && iconElement && {iconElement}} - {link.text} + + {link.text} + {link.isNew && }
@@ -175,7 +177,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ menuItem: css({ display: 'flex', alignItems: 'center', - gap: theme.spacing(0.5), + gap: theme.spacing(1.5), height: theme.spacing(4), paddingLeft: theme.spacing(0.5), position: 'relative', @@ -215,13 +217,13 @@ const getStyles = (theme: GrafanaTheme2) => ({ labelWrapper: css({ display: 'flex', alignItems: 'center', - gap: theme.spacing(2), + gap: theme.spacing(0.75), minWidth: 0, paddingLeft: theme.spacing(1), }), labelWrapperWithIcon: css({ paddingLeft: theme.spacing(0.5), - gap: theme.spacing(0.5), + gap: theme.spacing(0.75), }), hasActiveChild: css({ color: theme.colors.text.primary, diff --git a/public/app/core/components/AppChrome/MegaMenu/utils.test.ts b/public/app/core/components/AppChrome/MegaMenu/utils.test.ts index ea5a289bd95..e66c7c923c8 100644 --- a/public/app/core/components/AppChrome/MegaMenu/utils.test.ts +++ b/public/app/core/components/AppChrome/MegaMenu/utils.test.ts @@ -1,7 +1,10 @@ +import { cloneDeep } from 'lodash'; + import { NavModelItem } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; -import { enrichHelpItem, getActiveItem, findByUrl } from './utils'; +import { getEnrichedHelpItem, getActiveItem, findByUrl } from './utils'; const starredDashboardUid = 'foo'; const mockNavTree: NavModelItem[] = [ @@ -67,40 +70,62 @@ jest.mock('../../../app_events', () => ({ })); describe('enrichConfigItems', () => { - let mockHelpNode: NavModelItem; + let mockHelpNode: NavModelItem = { + id: 'help', + text: 'Help', + }; + let originalBuildInfo = { ...config.buildInfo }; - beforeEach(() => { - mockHelpNode = { - id: 'help', - text: 'Help', - }; + beforeAll(() => { + config.buildInfo.versionString = '9.0.0-test'; + }); + + afterAll(() => { + config.buildInfo = originalBuildInfo; }); it('enhances the help node with extra child links', () => { const contextSrv = new ContextSrv(); setContextSrv(contextSrv); - const helpNode = enrichHelpItem(mockHelpNode); - expect(helpNode!.children).toContainEqual( + const helpNode = getEnrichedHelpItem(mockHelpNode); + expect(helpNode.children).toContainEqual( expect.objectContaining({ text: 'Documentation', }) ); - expect(helpNode!.children).toContainEqual( + expect(helpNode.children).toContainEqual( expect.objectContaining({ text: 'Support', }) ); - expect(helpNode!.children).toContainEqual( + expect(helpNode.children).toContainEqual( expect.objectContaining({ text: 'Community', }) ); - expect(helpNode!.children).toContainEqual( + expect(helpNode.children).toContainEqual( expect.objectContaining({ text: 'Keyboard shortcuts', }) ); }); + + it('adds the version string as subtitle', () => { + const helpNode = getEnrichedHelpItem(mockHelpNode); + expect(helpNode.subTitle).toBe(config.buildInfo.versionString); + }); + + it("doesn't mutate the original help node", () => { + const originalHelpNode = cloneDeep(mockHelpNode); + const newHelpNode = getEnrichedHelpItem(mockHelpNode); + + // The mockHelpNode should remain deeply equal to the clone we made of it + expect(mockHelpNode).toEqual(originalHelpNode); + + // The new node should have a different identity than the original + expect(newHelpNode).not.toBe(mockHelpNode); + expect(newHelpNode.children).not.toBe(mockHelpNode.children); + }); }); describe('getActiveItem', () => { diff --git a/public/app/core/components/AppChrome/MegaMenu/utils.ts b/public/app/core/components/AppChrome/MegaMenu/utils.ts index 9996db02879..473e76ec834 100644 --- a/public/app/core/components/AppChrome/MegaMenu/utils.ts +++ b/public/app/core/components/AppChrome/MegaMenu/utils.ts @@ -13,14 +13,21 @@ import { HelpModal } from '../../help/HelpModal'; import { DOCK_MENU_BUTTON_ID, MEGA_MENU_HEADER_TOGGLE_ID } from './MegaMenuHeader'; -export const enrichHelpItem = (helpItem: NavModelItem) => { +const emitOpenShortcutsModal = () => { + appEvents.publish(new ShowModalReactEvent({ component: HelpModal })); +}; + +export const getEnrichedHelpItem = (helpItem: NavModelItem): NavModelItem => { let menuItems = helpItem.children || []; - if (helpItem.id === 'help') { - const onOpenShortcuts = () => { - appEvents.publish(new ShowModalReactEvent({ component: HelpModal })); - }; - helpItem.children = [ + if (helpItem.id !== 'help') { + return helpItem; + } + + return { + ...helpItem, + subTitle: config.buildInfo.versionString, + children: [ ...menuItems, ...getFooterLinks(), ...getEditionAndUpdateLinks(), @@ -28,11 +35,10 @@ export const enrichHelpItem = (helpItem: NavModelItem) => { id: 'keyboard-shortcuts', text: t('nav.help/keyboard-shortcuts', 'Keyboard shortcuts'), icon: 'keyboard', - onClick: onOpenShortcuts, + onClick: emitOpenShortcutsModal, }, - ]; - } - return helpItem; + ], + }; }; export const enrichWithInteractionTracking = ( diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx index 67ad47cded0..960b5f4085f 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { SelectableValue } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { Space, Text } from '@grafana/ui'; +import { Text } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { getUserOrganizations, setUserOrganization } from 'app/features/org/state/actions'; import { useDispatch, useSelector } from 'app/types/store'; @@ -33,12 +33,7 @@ export function OrganizationSwitcher() { }, [dispatch]); if (orgs?.length <= 1) { - return ( - <> - - {Branding.AppTitle} - - ); + return {Branding.AppTitle}; } return ; diff --git a/public/app/core/components/AppChrome/TopBar/HelpTopBarButton.tsx b/public/app/core/components/AppChrome/TopBar/HelpTopBarButton.tsx index 62aed5080ef..02215cda4fd 100644 --- a/public/app/core/components/AppChrome/TopBar/HelpTopBarButton.tsx +++ b/public/app/core/components/AppChrome/TopBar/HelpTopBarButton.tsx @@ -34,7 +34,12 @@ export const HelpTopBarButton = memo(function HelpTopBarButton({ isSmallScreen } if (isSmallScreen || !enrichedHelpNode.hideFromTabs || interactiveLearningPluginId === undefined) { return ( } placement="bottom-end"> - + ); } @@ -48,6 +53,14 @@ export const HelpTopBarButton = memo(function HelpTopBarButton({ isSmallScreen } icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} className={isOpen ? styles.helpButtonActive : undefined} + tooltip={ + isOpen + ? t( + 'navigation.help.interactive-learning.close-tooltip', + 'Close interactive learning, help, and documentation' + ) + : t('navigation.help.interactive-learning.open-tooltip', 'Open interactive learning, help, and documentation') + } onClick={() => { if (isOpen) { setDockedComponentId(undefined); diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx index 9e7e0ce7c7c..81f77230e26 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Components } from '@grafana/e2e-selectors'; @@ -6,6 +6,7 @@ import { ScopesContextValue } from '@grafana/runtime'; import { Stack, useStyles2 } from '@grafana/ui'; import { ScopesSelector } from 'app/features/scopes/selector/ScopesSelector'; +import { useExtensionSidebarContext } from '../ExtensionSidebar/ExtensionSidebarProvider'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight'; @@ -17,10 +18,14 @@ export interface Props { } export function SingleTopBarActions({ actions, breadcrumbActions, scopes }: Props) { - const styles = useStyles2(getStyles); + const { isOpen: isExtensionSidebarOpen, extensionSidebarWidth } = useExtensionSidebarContext(); + const styles = useStyles2(getStyles, extensionSidebarWidth); return ( -
+
{scopes?.state.enabled ? : undefined} @@ -33,7 +38,7 @@ export function SingleTopBarActions({ actions, breadcrumbActions, scopes }: Prop ); } -const getStyles = (theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2, extensionSidebarWidth = 0) => { return { actionsBar: css({ alignItems: 'center', @@ -43,5 +48,8 @@ const getStyles = (theme: GrafanaTheme2) => { height: getChromeHeaderLevelHeight(), padding: theme.spacing(0, 1, 0, 2), }), + constrained: css({ + maxWidth: `calc(100% - ${extensionSidebarWidth}px)`, + }), }; }; diff --git a/public/app/core/components/AppChrome/TopBar/useHelpNode.tsx b/public/app/core/components/AppChrome/TopBar/useHelpNode.tsx index 52beedffb56..fe9490520c2 100644 --- a/public/app/core/components/AppChrome/TopBar/useHelpNode.tsx +++ b/public/app/core/components/AppChrome/TopBar/useHelpNode.tsx @@ -1,11 +1,18 @@ import { cloneDeep } from 'lodash'; +import { useMemo } from 'react'; +import { NavModelItem } from '@grafana/data'; import { useSelector } from 'app/types/store'; -import { enrichHelpItem } from '../MegaMenu/utils'; +import { getEnrichedHelpItem } from '../MegaMenu/utils'; -export function useHelpNode() { +export function useHelpNode(): NavModelItem | undefined { const navIndex = useSelector((state) => state.navIndex); - const helpNode = cloneDeep(navIndex['help']); - return helpNode ? enrichHelpItem(helpNode) : undefined; + + const helpNode = useMemo(() => { + const helpNode = cloneDeep(navIndex['help']); + return helpNode ? getEnrichedHelpItem(helpNode) : undefined; + }, [navIndex]); + + return helpNode; } diff --git a/public/app/core/components/FolderFilter/FolderFilter.test.tsx b/public/app/core/components/FolderFilter/FolderFilter.test.tsx new file mode 100644 index 00000000000..06f0cf62a2a --- /dev/null +++ b/public/app/core/components/FolderFilter/FolderFilter.test.tsx @@ -0,0 +1,46 @@ +import { comboboxTestSetup } from 'test/helpers/comboboxTestSetup'; +import { render, screen, testWithFeatureToggles } from 'test/test-utils'; + +import { setBackendSrv } from '@grafana/runtime'; +import { setupMockServer } from '@grafana/test-utils/server'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { resetGrafanaSearcher } from 'app/features/search/service/searcher'; + +import { FolderFilter } from './FolderFilter'; +const [_, { folderA, folderB }] = getFolderFixtures(); + +setBackendSrv(backendSrv); +setupMockServer(); +comboboxTestSetup(); + +const fixtures: Array< + [ + // Test title + string, + // Feature toggle setup + Parameters[0], + ] +> = [ + ['app platform APIs enabled', { enable: ['unifiedStorageSearchUI'] }], + ['app platform APIs disabled', {}], +]; + +describe.each(fixtures)('FolderFilter - %s', (_title, featureToggleSetup) => { + beforeEach(() => { + resetGrafanaSearcher(); + }); + + testWithFeatureToggles(featureToggleSetup); + + it('allows selecting folders', async () => { + const onChange = jest.fn(); + const { user } = render(); + + await user.click(screen.getByPlaceholderText('Filter by folder')); + + await user.click(await screen.findByText(folderA.item.title)); + await user.click(await screen.findByText(folderB.item.title)); + expect(onChange).toHaveBeenCalledWith([folderA.item.uid, folderB.item.uid]); + }); +}); diff --git a/public/app/core/components/FolderFilter/FolderFilter.tsx b/public/app/core/components/FolderFilter/FolderFilter.tsx index c98490ea0c7..1f8f3e5557d 100644 --- a/public/app/core/components/FolderFilter/FolderFilter.tsx +++ b/public/app/core/components/FolderFilter/FolderFilter.tsx @@ -1,25 +1,17 @@ -import debounce from 'debounce-promise'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; -import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { AsyncMultiSelect, Icon } from '@grafana/ui'; +import { ComboboxOption, MultiCombobox } from '@grafana/ui'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; -import { FolderInfo } from 'app/types/folders'; export interface FolderFilterProps { - onChange: (folder: FolderInfo[]) => void; - maxMenuHeight?: number; + onChange: (folder: string[]) => void; } -export function FolderFilter({ onChange, maxMenuHeight }: FolderFilterProps): JSX.Element { - const [loading, setLoading] = useState(false); - const getOptions = useCallback((searchString: string) => getFoldersAsOptions(searchString, setLoading), []); - const debouncedLoadOptions = useMemo(() => debounce(getOptions, 300), [getOptions]); - - const [value, setValue] = useState>>([]); +export function FolderFilter({ onChange }: FolderFilterProps): JSX.Element { + const [value, setValue] = useState([]); const onSelectOptionChange = useCallback( - (folders: Array>) => { + (folders: ComboboxOption[]) => { const changedFolderIds = folders.filter((f) => Boolean(f.value)).map((f) => f.value!); onChange(changedFolderIds); setValue(folders); @@ -28,26 +20,21 @@ export function FolderFilter({ onChange, maxMenuHeight }: FolderFilterProps): JS ); return ( - } aria-label={t('folder-filter.select-aria-label', 'Folder filter')} - defaultOptions /> ); } -async function getFoldersAsOptions( - searchString: string, - setLoading: (loading: boolean) => void -): Promise>> { - setLoading(true); +async function getFoldersAsOptions(searchString: string) { // Use searcher as it will handle the logic for using the appropriate API const searcher = getGrafanaSearcher(); const queryResponse = await searcher.search({ @@ -59,13 +46,12 @@ async function getFoldersAsOptions( const options = queryResponse.view.map((item) => ({ label: item.name, - value: { uid: item.uid, title: item.name }, + value: item.uid, })); if (!searchString || 'dashboards'.includes(searchString.toLowerCase())) { - options.unshift({ label: 'Dashboards', value: { uid: 'general', title: 'Dashboards' } }); + options.unshift({ label: 'Dashboards', value: 'general' }); } - setLoading(false); return options; } diff --git a/public/app/core/components/GraphNG/GraphNG.tsx b/public/app/core/components/GraphNG/GraphNG.tsx index 3babaa7ac63..7094c137501 100644 --- a/public/app/core/components/GraphNG/GraphNG.tsx +++ b/public/app/core/components/GraphNG/GraphNG.tsx @@ -1,5 +1,5 @@ -import { Component } from 'react'; import * as React from 'react'; +import { Component } from 'react'; import uPlot, { AlignedData } from 'uplot'; import { @@ -16,7 +16,7 @@ import { } from '@grafana/data'; import { DashboardCursorSync, VizLegendOptions } from '@grafana/schema'; import { Themeable2, VizLayout } from '@grafana/ui'; -import { UPlotChart, AxisProps, Renderers, UPlotConfigBuilder, ScaleProps, pluginLog } from '@grafana/ui/internal'; +import { AxisProps, pluginLog, Renderers, ScaleProps, UPlotChart, UPlotConfigBuilder } from '@grafana/ui/internal'; import { GraphNGLegendEvent, XYFieldMatchers } from './types'; import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; @@ -40,7 +40,12 @@ export interface GraphNGProps extends Themeable2 { tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps; onLegendClick?: (event: GraphNGLegendEvent) => void; children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode; - prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder; + prepConfig: ( + alignedFrame: DataFrame, + allFrames: DataFrame[], + getTimeRange: () => TimeRange, + annotationLanes?: number + ) => UPlotConfigBuilder; propsToDiff?: Array; preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null; renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; @@ -63,6 +68,9 @@ export interface GraphNGProps extends Themeable2 { * similar to structureRev. then we can drop propsToDiff entirely. */ options?: Record; + + // Annotation lanes count + annotationLanes?: number; } function sameProps>( @@ -191,7 +199,7 @@ export class GraphNG extends Component { let config = this.state?.config; if (withConfig) { - config = props.prepConfig(alignedFrameFinal, this.props.frames, this.getTimeRange); + config = props.prepConfig(alignedFrameFinal, this.props.frames, this.getTimeRange, this.props.annotationLanes); pluginLog('GraphNG', false, 'config prepared', config); } @@ -229,7 +237,12 @@ export class GraphNG extends Component { propsChanged; if (shouldReconfig) { - newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange); + newState.config = this.props.prepConfig( + newState.alignedFrame, + this.props.frames, + this.getTimeRange, + this.props.annotationLanes + ); pluginLog('GraphNG', false, 'config recreated', newState.config); } diff --git a/public/app/core/components/Select/ServiceAccountPicker.tsx b/public/app/core/components/Select/ServiceAccountPicker.tsx index 037404dcb2b..e9b200a4cb0 100644 --- a/public/app/core/components/Select/ServiceAccountPicker.tsx +++ b/public/app/core/components/Select/ServiceAccountPicker.tsx @@ -1,6 +1,6 @@ import debounce from 'debounce-promise'; import { isNil } from 'lodash'; -import { Component } from 'react'; +import { useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -14,66 +14,58 @@ export interface Props { inputId?: string; } -export interface State { - isLoading: boolean; -} +export const ServiceAccountPicker = ({ className, onSelected, inputId }: Props) => { + const [isLoading, setIsLoading] = useState(false); -export class ServiceAccountPicker extends Component { - constructor(props: Props) { - super(props); - this.state = { isLoading: false }; - } + const search = useMemo( + () => + debounce( + async (query?: string) => { + setIsLoading(true); - search = debounce( - async (query?: string) => { - this.setState({ isLoading: true }); + if (isNil(query)) { + query = ''; + } - if (isNil(query)) { - query = ''; - } - - return getBackendSrv() - .get(`/api/serviceaccounts/search?query=${query}&perpage=100`) - .then((result: ServiceAccountsState) => { - return result.serviceAccounts.map((sa) => ({ - id: sa.id, - uid: sa.uid, - value: sa, - label: sa.login, - imgUrl: sa.avatarUrl, - login: sa.login, - })); - }) - .finally(() => { - this.setState({ isLoading: false }); - }); - }, - 300, - { leading: true } + return getBackendSrv() + .get(`/api/serviceaccounts/search?query=${query}&perpage=100`) + .then((result: ServiceAccountsState) => { + return result.serviceAccounts.map((sa) => ({ + id: sa.id, + uid: sa.uid, + value: sa, + label: sa.login, + imgUrl: sa.avatarUrl, + login: sa.login, + })); + }) + .finally(() => { + setIsLoading(false); + }); + }, + 300, + { leading: true } + ), + [] ); - render() { - const { className, onSelected, inputId } = this.props; - const { isLoading } = this.state; - - return ( -
- -
- ); - } -} + return ( +
+ +
+ ); +}; diff --git a/public/app/core/components/Select/UserPicker.tsx b/public/app/core/components/Select/UserPicker.tsx index 5284cbeca5f..e7d4110d09b 100644 --- a/public/app/core/components/Select/UserPicker.tsx +++ b/public/app/core/components/Select/UserPicker.tsx @@ -1,6 +1,6 @@ import debounce from 'debounce-promise'; import { isNil } from 'lodash'; -import { Component } from 'react'; +import { useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -14,63 +14,55 @@ export interface Props { inputId?: string; } -export interface State { - isLoading: boolean; -} +export const UserPicker = ({ className, onSelected, inputId }: Props) => { + const [isLoading, setIsLoading] = useState(false); -export class UserPicker extends Component { - constructor(props: Props) { - super(props); - this.state = { isLoading: false }; - } + const search = useMemo( + () => + debounce( + async (query?: string) => { + setIsLoading(true); - search = debounce( - async (query?: string) => { - this.setState({ isLoading: true }); + if (isNil(query)) { + query = ''; + } - if (isNil(query)) { - query = ''; - } - - return getBackendSrv() - .get(`/api/org/users/lookup?query=${query}&limit=100`) - .then((result: OrgUser[]) => { - return result.map((user) => ({ - id: user.userId, - uid: user.uid, - value: user, - label: user.login, - imgUrl: user.avatarUrl, - login: user.login, - })); - }) - .finally(() => { - this.setState({ isLoading: false }); - }); - }, - 300, - { leading: true } + return getBackendSrv() + .get(`/api/org/users/lookup?query=${query}&limit=100`) + .then((result: OrgUser[]) => { + return result.map((user) => ({ + id: user.userId, + uid: user.uid, + value: user, + label: user.login, + imgUrl: user.avatarUrl, + login: user.login, + })); + }) + .finally(() => { + setIsLoading(false); + }); + }, + 300, + { leading: true } + ), + [] ); - render() { - const { className, onSelected, inputId } = this.props; - const { isLoading } = this.state; - - return ( -
- -
- ); - } -} + return ( +
+ +
+ ); +}; diff --git a/public/app/core/components/TimeSeries/TimeSeries.tsx b/public/app/core/components/TimeSeries/TimeSeries.tsx index 5af30da15c5..8fd11f1bc37 100644 --- a/public/app/core/components/TimeSeries/TimeSeries.tsx +++ b/public/app/core/components/TimeSeries/TimeSeries.tsx @@ -6,14 +6,19 @@ import { hasVisibleLegendSeries, PlotLegend, UPlotConfigBuilder } from '@grafana import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG'; -import { preparePlotConfigBuilder } from './utils'; +import { getXAxisConfig, preparePlotConfigBuilder } from './utils'; -const propsToDiff: Array = ['legend', 'options', 'theme']; +const propsToDiff: Array = ['legend', 'options', 'annotationLanes', 'theme']; type TimeSeriesProps = Omit; export class UnthemedTimeSeries extends Component { - prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { + prepConfig = ( + alignedFrame: DataFrame, + allFrames: DataFrame[], + getTimeRange: () => TimeRange, + annotationLanes?: number + ) => { const { theme, timeZone, options, renderers, tweakAxis, tweakScale } = this.props; return preparePlotConfigBuilder({ @@ -27,6 +32,7 @@ export class UnthemedTimeSeries extends Component { tweakAxis, hoverProximity: options?.tooltip?.hoverProximity, orientation: options?.orientation, + xAxisConfig: getXAxisConfig(annotationLanes), }); }; diff --git a/public/app/core/components/TimeSeries/utils.test.ts b/public/app/core/components/TimeSeries/utils.test.ts index 583358c7c4c..6168fb8ddc5 100644 --- a/public/app/core/components/TimeSeries/utils.test.ts +++ b/public/app/core/components/TimeSeries/utils.test.ts @@ -1,7 +1,7 @@ -import { EventBus, FieldType } from '@grafana/data'; +import { createDataFrame, dateTime, DateTimeInput, EventBus, FieldType } from '@grafana/data'; import { getTheme } from '@grafana/ui'; -import { preparePlotConfigBuilder } from './utils'; +import { getXAxisConfig, preparePlotConfigBuilder, UPLOT_DEFAULT_AXIS_GAP } from './utils'; describe('when fill below to option is used', () => { let eventBus: EventBus; @@ -272,3 +272,129 @@ describe('when fill below to option is used', () => { } }); }); + +describe('time axis units', () => { + it('should use default time unit formatting if no custom unit provided ', () => { + const frame = createDataFrame({ + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + type: FieldType.time, + }, + { + config: {}, + values: [1, 2, 3], + name: 'Value', + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'Value', + type: FieldType.number, + }, + ], + }); + const eventBus = { + publish: jest.fn(), + getStream: jest.fn(), + subscribe: jest.fn(), + removeAllListeners: jest.fn(), + newScopedBus: jest.fn(), + }; + const builder = preparePlotConfigBuilder({ + frame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: [frame], + renderers: [], + }); + const config = builder.getConfig(); + expect(config.axes![0]!.values).toEqual(expect.any(Function)); + // @ts-ignore + expect(config.axes![0]!.values(config, [1667406900000, 1761316576114], 0, 100, 1000)).toEqual([ + '11:35:00', + '09:36:16', + ]); + }); + + it('should use custom time unit if provided ', () => { + const frame = createDataFrame({ + fields: [ + { + config: { unit: 'time: MM-DD' }, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + display: jest.fn((v) => ({ text: dateTime(v as DateTimeInput).format('MM-DD'), numeric: Number(v) })), + }, + { + config: {}, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + }); + const eventBus = { + publish: jest.fn(), + getStream: jest.fn(), + subscribe: jest.fn(), + removeAllListeners: jest.fn(), + newScopedBus: jest.fn(), + }; + const builder = preparePlotConfigBuilder({ + frame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: [frame], + renderers: [], + }); + const config = builder.getConfig(); + expect(config.axes![0]!.values).toEqual(expect.any(Function)); + // @ts-ignore + expect(config.axes![0]!.values(config, [1667406900000, 1761316576114], 0, 100, 1000)).toEqual(['11-02', '10-24']); + }); +}); + +describe('calculateAnnotationLaneSizes', () => { + it('should not regress', () => { + expect(getXAxisConfig()).toEqual(undefined); + expect(getXAxisConfig(0)).toEqual(undefined); + }); + it('should return config to resize x-axis size, gap, and ticks size', () => { + expect(getXAxisConfig(2)).toEqual({ + gap: UPLOT_DEFAULT_AXIS_GAP, + size: 36, + ticks: { + size: 19, + }, + }); + expect(getXAxisConfig(3)).toEqual({ + gap: UPLOT_DEFAULT_AXIS_GAP, + size: 43, + ticks: { + size: 26, + }, + }); + }); +}); diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index 1bf0c5a18c0..82c18cdd359 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -68,8 +68,15 @@ import { buildScaleKey, getStackingGroups, preparePlotData2, + AxisProps, } from '@grafana/ui/internal'; +import { ANNOTATION_LANE_SIZE } from '../../../plugins/panel/timeseries/plugins/utils'; + +// See UPlotAxisBuilder.ts::calculateAxisSize for default axis size calculation +export const UPLOT_DEFAULT_AXIS_SIZE = 17; +export const UPLOT_DEFAULT_AXIS_GAP = 5; + const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals)); const defaultConfig: GraphFieldConfig = { @@ -90,6 +97,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ tweakAxis = (opts) => opts, hoverProximity, orientation = VizOrientation.Horizontal, + xAxisConfig, }) => { // we want the Auto and Horizontal orientation to default to Horizontal const isHorizontal = orientation !== VizOrientation.Vertical; @@ -156,6 +164,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ theme, grid: { show: i === 0 && xField.config.custom?.axisGridShow }, filter: filterTicks, + formatValue: xField.config.unit?.startsWith('time:') + ? (v, decimals) => xField.display!(v, decimals).text + : undefined, + ...xAxisConfig, }); } @@ -706,3 +718,25 @@ function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map | undefined { + if (lanes > 1) { + const annotationLanesSize = lanes * ANNOTATION_LANE_SIZE; + // Add an extra lane's worth of height below the annotation lanes in order to show the gridlines through the annotation lanes + const axisSize = annotationLanesSize + UPLOT_DEFAULT_AXIS_GAP; + // Consistent gap between gridlines and x-axis labels + const gap = UPLOT_DEFAULT_AXIS_GAP; + // Axis size is: default size + gap size + annotationLaneSize + const size = UPLOT_DEFAULT_AXIS_SIZE + gap + annotationLanesSize; + + return { + size, + gap, + ticks: { + size: axisSize, + }, + }; + } + + return undefined; +} diff --git a/public/app/core/components/TimelineChart/TimelineChart.tsx b/public/app/core/components/TimelineChart/TimelineChart.tsx index f4317a127b7..4b677c72282 100644 --- a/public/app/core/components/TimelineChart/TimelineChart.tsx +++ b/public/app/core/components/TimelineChart/TimelineChart.tsx @@ -1,10 +1,11 @@ -import { Component } from 'react'; +import { useCallback } from 'react'; import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data'; -import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema'; +import { TimelineValueAlignment, TooltipDisplayMode, VisibilityMode, VizTooltipOptions } from '@grafana/schema'; import { UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui'; import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG'; +import { getXAxisConfig } from '../TimeSeries/utils'; import { preparePlotConfigBuilder, TimelineMode } from './utils'; @@ -23,71 +24,88 @@ export interface TimelineProps extends Omit { - getValueColor = (frameIdx: number, fieldIdx: number, value: unknown) => { - const field = this.props.frames[frameIdx]?.fields[fieldIdx]; +export const TimelineChart = (props: TimelineProps) => { + const { frames, timeZone, rowHeight, tooltip, legend, legendItems } = props; - if (field?.display) { - const disp = field.display(value); // will apply color modes - if (disp.color) { - return disp.color; + const getValueColor = useCallback( + (frameIdx: number, fieldIdx: number, value: unknown) => { + const field = frames[frameIdx]?.fields[fieldIdx]; + + if (field?.display) { + const disp = field.display(value); // will apply color modes + if (disp.color) { + return disp.color; + } } - } - return FALLBACK_COLOR; - }; + return FALLBACK_COLOR; + }, + [frames] + ); - prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { - return preparePlotConfigBuilder({ - frame: alignedFrame, - getTimeRange, - allFrames: this.props.frames, - ...this.props, + const prepConfig = useCallback( + (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { + return preparePlotConfigBuilder({ + frame: alignedFrame, + getTimeRange, + allFrames: frames, + ...props, - // Ensure timezones is passed as an array - timeZones: Array.isArray(this.props.timeZone) ? this.props.timeZone : [this.props.timeZone], + // Ensure timezones is passed as an array + timeZones: Array.isArray(timeZone) ? timeZone : [timeZone], - // When there is only one row, use the full space - rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1, - getValueColor: this.getValueColor, + // When there is only one row, use the full space + rowHeight: alignedFrame.fields.length > 2 ? rowHeight : 1, + getValueColor: getValueColor, - hoverMulti: this.props.tooltip?.mode === TooltipDisplayMode.Multi, - }); - }; + hoverMulti: tooltip?.mode === TooltipDisplayMode.Multi, + xAxisConfig: getXAxisConfig(props.annotationLanes), + }); + }, + [frames, props, timeZone, rowHeight, getValueColor, tooltip] + ); - renderLegend = (config: UPlotConfigBuilder) => { - const { legend, legendItems } = this.props; + const renderLegend = useCallback( + (config: UPlotConfigBuilder) => { + if (!config || !legendItems || !legend || legend.showLegend === false) { + return null; + } - if (!config || !legendItems || !legend || legend.showLegend === false) { - return null; - } + return ( + + + + ); + }, + [legend, legendItems] + ); - return ( - - - - ); - }; - - render() { - return ( - f.type === FieldType.time, - y: (f) => - f.type === FieldType.number || - f.type === FieldType.boolean || - f.type === FieldType.string || - f.type === FieldType.enum, - }} - prepConfig={this.prepConfig} - propsToDiff={propsToDiff} - renderLegend={this.renderLegend} - omitHideFromViz={true} - /> - ); - } -} + return ( + f.type === FieldType.time, + y: (f) => + f.type === FieldType.number || + f.type === FieldType.boolean || + f.type === FieldType.string || + f.type === FieldType.enum, + }} + prepConfig={prepConfig} + propsToDiff={propsToDiff} + renderLegend={renderLegend} + omitHideFromViz={true} + /> + ); +}; diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 1b233d4a3ad..a2e9ad366ff 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -93,6 +93,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( mergeValues, getValueColor, hoverMulti, + xAxisConfig, }) => { const builder = new UPlotConfigBuilder(timeZones[0]); @@ -166,7 +167,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( range: coreConfig.yRange, }); - const xAxisHidden = frame.fields[0].config.custom.axisPlacement === AxisPlacement.Hidden; + const xField = frame.fields[0]; + const xAxisHidden = xField.config.custom.axisPlacement === AxisPlacement.Hidden; builder.addAxis({ show: !xAxisHidden, @@ -176,6 +178,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( placement: AxisPlacement.Bottom, timeZone: timeZones[0], theme, + formatValue: xField.config.unit?.startsWith('time:') + ? (v, decimals) => xField.display!(v, decimals).text + : undefined, + ...xAxisConfig, }); const yCustomConfig = frame.fields[1].config.custom; diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 890879ca7a2..d86b93c916e 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -3,6 +3,7 @@ import { AnyAction, combineReducers } from 'redux'; import { notificationsAPIv0alpha1, rulesAPIv0alpha1 } from '@grafana/alerting/unstable'; import { allReducers as allApiClientReducers } from '@grafana/api-clients/rtkq'; +import { generatedAPI as legacyAPI } from '@grafana/api-clients/rtkq/legacy'; import sharedReducers from 'app/core/reducers'; import ldapReducers from 'app/features/admin/state/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; @@ -14,7 +15,6 @@ import panelEditorReducers from 'app/features/dashboard/components/PanelEditor/s import dashboardReducers from 'app/features/dashboard/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import exploreReducers from 'app/features/explore/state/main'; -import foldersReducers from 'app/features/folders/state/reducers'; import invitesReducers from 'app/features/invites/state/reducers'; import importDashboardReducers from 'app/features/manage-dashboards/state/reducers'; import organizationReducers from 'app/features/org/state/reducers'; @@ -34,7 +34,6 @@ const rootReducers = { ...sharedReducers, ...alertingReducers, ...teamsReducers, - ...foldersReducers, ...dashboardReducers, ...exploreReducers, ...dataSourcesReducers, @@ -51,6 +50,7 @@ const rootReducers = { ...templatingReducers, ...supportBundlesReducer, ...authConfigReducers, + [legacyAPI.reducerPath]: legacyAPI.reducer, plugins: pluginsReducer, [alertingApi.reducerPath]: alertingApi.reducer, [notificationsAPIv0alpha1.reducerPath]: notificationsAPIv0alpha1.reducer, diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 193a03a6847..2f6aed7e115 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -654,7 +654,12 @@ export class BackendSrv implements BackendService { return this.get(`/api/public/dashboards/${uid}`); } + /** + * @deprecated Use getFolderByUidFacade from app/api/clients/folder/v1beta1/hooks instead + * or manually handle calling legacy vs app platform API based on feature toggles + */ getFolderByUid(uid: string, options: FolderRequestOptions = {}) { + deprecationWarning('backend_srv', 'getFolderByUid(uid)', 'getFolderByUidFacade(uid)'); const queryParams = new URLSearchParams(); if (options.withAccessControl) { queryParams.set('accesscontrol', 'true'); diff --git a/public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts b/public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts index 78db86015cd..07c89caf553 100644 --- a/public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts +++ b/public/app/core/services/echo/backends/analytics/ApplicationInsightsBackend.ts @@ -23,6 +23,7 @@ declare global { export interface ApplicationInsightsBackendOptions { connectionString: string; endpointUrl?: string; + autoRouteTracking?: boolean; } export class ApplicationInsightsBackend implements EchoBackend { @@ -33,6 +34,7 @@ export class ApplicationInsightsBackend implements EchoBackend { it('builds short URL with metadata name and namespace', () => { const shortUrl: ShortURL = { kind: 'ShortURL', - apiVersion: 'shorturl.grafana.app/v1alpha1', + apiVersion: 'shorturl.grafana.app/v1beta1', metadata: { name: 'abc123def', namespace: 'org-5', @@ -140,7 +140,7 @@ describe('buildShortUrl', () => { const shortUrl: ShortURL = { kind: 'ShortURL', - apiVersion: 'shorturl.grafana.app/v1alpha1', + apiVersion: 'shorturl.grafana.app/v1beta1', metadata: { name: 'xyz789', namespace: 'org-1', diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index c01988d2262..536911f9942 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -1,17 +1,17 @@ import memoizeOne from 'memoize-one'; -import { generatedAPI } from '@grafana/api-clients/rtkq/shorturl/v1alpha1'; import { AbsoluteTimeRange, LogRowModel, UrlQueryMap } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv, config, locationService } from '@grafana/runtime'; import { sceneGraph, SceneTimeRangeLike, VizPanel } from '@grafana/scenes'; +import { shortURLAPIv1beta1 } from 'app/api/clients/shorturl/v1beta1'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { dispatch } from 'app/store/store'; -import { ShortURL } from '../../../../apps/shorturl/plugin/src/generated/shorturl/v1alpha1/shorturl_object_gen'; +import { ShortURL } from '../../../../apps/shorturl/plugin/src/generated/shorturl/v1beta1/shorturl_object_gen'; import { extractErrorMessage } from '../../api/utils'; import { ShareLinkConfiguration } from '../../features/dashboard-scene/sharing/ShareButton/utils'; @@ -46,9 +46,9 @@ export const createShortLink = async function (path: string) { if (config.featureToggles.useKubernetesShortURLsAPI) { // Use RTK API - it handles caching/failures/retries automatically const result = await dispatch( - generatedAPI.endpoints.createShortUrl.initiate({ + shortURLAPIv1beta1.endpoints.createShortUrl.initiate({ shortUrl: { - apiVersion: 'shorturl.grafana.app/v1alpha1', + apiVersion: 'shorturl.grafana.app/v1beta1', kind: 'ShortURL', metadata: {}, spec: { diff --git a/public/app/features/admin/UserLdapSyncInfo.tsx b/public/app/features/admin/UserLdapSyncInfo.tsx index 1baf9e219a2..4277fa4c814 100644 --- a/public/app/features/admin/UserLdapSyncInfo.tsx +++ b/public/app/features/admin/UserLdapSyncInfo.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { dateTimeFormat } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; @@ -16,80 +16,72 @@ interface Props { onUserSync: () => void; } -interface State {} - const format = 'dddd YYYY-MM-DD HH:mm zz'; const debugLDAPMappingBaseURL = '/admin/authentication/ldap'; -export class UserLdapSyncInfo extends PureComponent { - onUserSync = () => { - this.props.onUserSync(); - }; +export const UserLdapSyncInfo = memo(({ ldapSyncInfo, user, onUserSync }: Props) => { + const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync; + const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : ''; + const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?username=${user && user.login}`; + const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead); + const canSyncLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersSync); - render() { - const { ldapSyncInfo, user } = this.props; - const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync; - const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : ''; - const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?username=${user && user.login}`; - const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead); - const canSyncLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersSync); - - return ( - <> -

- LDAP Synchronisation -

-
-
-
- - - - - - - - - - - -
- External sync - - - User synced via LDAP. Some changes must be done in LDAP or mappings. - - - -
- Next scheduled synchronization - - {ldapSyncInfo.enabled ? ( - nextSyncTime - ) : ( - Not enabled - )} -
-
-
- {canSyncLDAPUser && ( - - )} - {canReadLDAPUser && ( - - Debug LDAP Mapping - - )} -
+ return ( + <> +

+ LDAP Synchronisation +

+
+
+ + + + + + + + + + + + +
+ External sync + + + User synced via LDAP. Some changes must be done in LDAP or mappings. + + + +
+ Next scheduled synchronization + + {ldapSyncInfo.enabled ? ( + nextSyncTime + ) : ( + Not enabled + )} +
- - ); - } -} +
+ {canSyncLDAPUser && ( + + )} + {canReadLDAPUser && ( + + Debug LDAP Mapping + + )} +
+
+ + ); +}); +UserLdapSyncInfo.displayName = 'UserLdapSyncInfo'; diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index b7083bf7051..a0169a1022a 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { createRef, PureComponent, ReactElement } from 'react'; +import { memo, PureComponent, ReactElement, useEffect, useRef, useState } from 'react'; import { GrafanaTheme2, OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; @@ -10,10 +10,8 @@ import { Icon, Modal, stylesFactory, - Themeable2, Tooltip, useStyles2, - withTheme2, Stack, TextLink, } from '@grafana/ui'; @@ -37,73 +35,63 @@ interface Props { onOrgAdd: (orgId: number, role: OrgRole) => void; } -interface State { - showAddOrgModal: boolean; -} +export const UserOrgs = memo(({ user, orgs, isExternalUser, onOrgRoleChange, onOrgRemove, onOrgAdd }: Props) => { + const [showAddOrgModal, setShowAddOrgModal] = useState(false); + const addToOrgButtonRef = useRef(null); -export class UserOrgs extends PureComponent { - addToOrgButtonRef = createRef(); - state = { - showAddOrgModal: false, + const showOrgAddModal = () => { + setShowAddOrgModal(true); }; - showOrgAddModal = () => { - this.setState({ showAddOrgModal: true }); + const dismissOrgAddModal = () => { + setShowAddOrgModal(false); + addToOrgButtonRef.current?.focus(); }; - dismissOrgAddModal = () => { - this.setState({ showAddOrgModal: false }, () => { - this.addToOrgButtonRef.current?.focus(); - }); - }; + const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) && !isExternalUser; - render() { - const { user, orgs, isExternalUser, onOrgRoleChange, onOrgRemove, onOrgAdd } = this.props; - const { showAddOrgModal } = this.state; + return ( +
+

+ Organizations +

+ + + + {orgs.map((org, index) => ( + + ))} + +
- const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) && !isExternalUser; - return ( -
-

- Organizations -

- - - - {orgs.map((org, index) => ( - - ))} - -
+
+ {canAddToOrg && ( + + )} +
+ +
+
+ ); +}); +UserOrgs.displayName = 'UserOrgs'; -
- {canAddToOrg && ( - - )} -
- -
-
- ); - } -} - -const getOrgRowStyles = stylesFactory((theme: GrafanaTheme2) => { +const getOrgRowStyles = (theme: GrafanaTheme2) => { return { removeButton: css({ marginRight: '0.6rem', @@ -130,9 +118,9 @@ const getOrgRowStyles = stylesFactory((theme: GrafanaTheme2) => { marginRight: theme.spacing(1), }), }; -}); +}; -interface OrgRowProps extends Themeable2 { +interface OrgRowProps { user?: UserDTO; org: UserOrg; isExternalUser?: boolean; @@ -140,124 +128,116 @@ interface OrgRowProps extends Themeable2 { onOrgRoleChange: (orgId: number, newRole: OrgRole) => void; } -class UnThemedOrgRow extends PureComponent { - state = { - currentRole: this.props.org.role, - isChangingRole: false, - roleOptions: [], - }; +const OrgRow = memo(({ user, org, isExternalUser, onOrgRemove, onOrgRoleChange }: OrgRowProps) => { + const [currentRole, setCurrentRole] = useState(org.role); + const [isChangingRole, setIsChangingRole] = useState(false); + const [roleOptions, setRoleOptions] = useState([]); + const styles = useStyles2(getOrgRowStyles); - componentDidMount() { + useEffect(() => { if (contextSrv.licensedAccessControlEnabled()) { if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { - fetchRoleOptions(this.props.org.orgId) - .then((roles) => this.setState({ roleOptions: roles })) + fetchRoleOptions(org.orgId) + .then((roles) => setRoleOptions(roles)) .catch((e) => console.error(e)); } } - } + }, [org.orgId]); - onOrgRemove = async () => { - const { org } = this.props; - this.props.onOrgRemove(org.orgId); + const handleOrgRemove = async () => { + onOrgRemove(org.orgId); }; - onChangeRoleClick = () => { - const { org } = this.props; - this.setState({ isChangingRole: true, currentRole: org.role }); + const handleChangeRoleClick = () => { + setIsChangingRole(true); + setCurrentRole(org.role); }; - onOrgRoleChange = (newRole: OrgRole) => { - this.setState({ currentRole: newRole }); + const handleOrgRoleChange = (newRole: OrgRole) => { + setCurrentRole(newRole); }; - onOrgRoleSave = () => { - this.props.onOrgRoleChange(this.props.org.orgId, this.state.currentRole); + const handleOrgRoleSave = () => { + onOrgRoleChange(org.orgId, currentRole); }; - onCancelClick = () => { - this.setState({ isChangingRole: false }); + const handleCancelClick = () => { + setIsChangingRole(false); }; - onBasicRoleChange = (newRole: OrgRole) => { - this.props.onOrgRoleChange(this.props.org.orgId, newRole); + const handleBasicRoleChange = (newRole: OrgRole) => { + onOrgRoleChange(org.orgId, newRole); }; - render() { - const { user, org, isExternalUser, theme } = this.props; - const authSource = user?.authLabels?.length && user?.authLabels[0]; - const lockMessage = authSource ? `Synced via ${authSource}` : ''; - const { currentRole, isChangingRole } = this.state; - const styles = getOrgRowStyles(theme); - const labelClass = cx('width-16', styles.label); - const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite); - const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove) && !isExternalUser; - const rolePickerDisabled = isExternalUser || !canChangeRole; + const authSource = user?.authLabels?.length && user?.authLabels[0]; + const lockMessage = authSource ? `Synced via ${authSource}` : ''; + const labelClass = cx('width-16', styles.label); + const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite); + const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove) && !isExternalUser; + const rolePickerDisabled = isExternalUser || !canChangeRole; - const inputId = `${org.name}-input`; - return ( - - - - - {contextSrv.licensedAccessControlEnabled() ? ( - -
-
- -
- {isExternalUser && } + const inputId = `${org.name}-input`; + return ( + + + + + {contextSrv.licensedAccessControlEnabled() ? ( + +
+
+
- - ) : ( - <> - {isChangingRole ? ( - - - - ) : ( - {org.role} - )} - - {canChangeRole && ( - - )} - - - )} - - {canRemoveFromOrg && ( - - {t('admin.user-orgs.remove-button', 'Remove from organization')} - - )} + {isExternalUser && } +
- - ); - } -} - -const OrgRow = withTheme2(UnThemedOrgRow); + ) : ( + <> + {isChangingRole ? ( + + + + ) : ( + {org.role} + )} + + {canChangeRole && ( + + )} + + + )} + + {canRemoveFromOrg && ( + + {t('admin.user-orgs.remove-button', 'Remove from organization')} + + )} + + + ); +}); +OrgRow.displayName = 'OrgRow'; const getAddToOrgModalStyles = stylesFactory(() => ({ modal: css({ diff --git a/public/app/features/admin/UserSessions.tsx b/public/app/features/admin/UserSessions.tsx index f05278d7884..16fadaa8313 100644 --- a/public/app/features/admin/UserSessions.tsx +++ b/public/app/features/admin/UserSessions.tsx @@ -1,4 +1,4 @@ -import { createRef, PureComponent } from 'react'; +import { memo, useRef, useState } from 'react'; import { Trans, t } from '@grafana/i18n'; import { ConfirmButton, ConfirmModal, Button, Stack } from '@grafana/ui'; @@ -15,120 +15,107 @@ interface Props { onAllSessionsRevoke: () => void; } -interface State { - showLogoutModal: boolean; -} +export const UserSessions = memo(({ sessions, onSessionRevoke, onAllSessionsRevoke }: Props) => { + const [showLogoutModal, setShowLogoutModal] = useState(false); + const forceAllLogoutButton = useRef(null); -class BaseUserSessions extends PureComponent { - forceAllLogoutButton = createRef(); - state: State = { - showLogoutModal: false, + const showLogoutConfirmationModal = () => { + setShowLogoutModal(true); }; - showLogoutConfirmationModal = () => { - this.setState({ showLogoutModal: true }); + const dismissLogoutConfirmationModal = () => { + setShowLogoutModal(false); + forceAllLogoutButton.current?.focus(); }; - dismissLogoutConfirmationModal = () => { - this.setState({ showLogoutModal: false }, () => { - this.forceAllLogoutButton.current?.focus(); - }); - }; - - onSessionRevoke = (id: number) => { + const handleSessionRevoke = (id: number) => { return () => { - this.props.onSessionRevoke(id); + onSessionRevoke(id); }; }; - onAllSessionsRevoke = () => { - this.setState({ showLogoutModal: false }); - this.props.onAllSessionsRevoke(); + const handleAllSessionsRevoke = () => { + setShowLogoutModal(false); + onAllSessionsRevoke(); }; - render() { - const { sessions } = this.props; - const { showLogoutModal } = this.state; + const canLogout = contextSrv.hasPermission(AccessControlAction.UsersLogout); - const canLogout = contextSrv.hasPermission(AccessControlAction.UsersLogout); + return ( +
+

+ Sessions +

+ +
+ + + + + + + + + + + + {sessions && + sessions.map((session, index) => ( + + + + + + + + + ))} + +
+ Last seen + + Logged on + + IP address + + Browser and OS + + Identity Provider +
{session.isActive ? t('admin.user-sessions.now', 'Now') : session.seenAt}{formatDate(session.createdAt, { dateStyle: 'long' })}{session.clientIp}{`${session.browser} on ${session.os} ${session.osVersion}`} + {session.authModule && } + + {canLogout && ( + + {t('admin.user-sessions.force-logout-button', 'Force logout')} + + )} +
+
- return ( -
-

- Sessions -

- -
- - - - - - - - - - - - {sessions && - sessions.map((session, index) => ( - - - - - - - - - ))} - -
- Last seen - - Logged on - - IP address - - Browser and OS - - Identity Provider -
{session.isActive ? t('admin.user-sessions.now', 'Now') : session.seenAt}{formatDate(session.createdAt, { dateStyle: 'long' })}{session.clientIp}{`${session.browser} on ${session.os} ${session.osVersion}`} - {session.authModule && } - - {canLogout && ( - - {t('admin.user-sessions.force-logout-button', 'Force logout')} - - )} -
-
- -
- {canLogout && sessions.length > 0 && ( - +
+ {canLogout && sessions.length > 0 && ( + + )} + -
- -
- ); - } -} - -export const UserSessions = BaseUserSessions; + confirmText={t('admin.base-user-sessions.confirmText-force-logout', 'Force logout')} + onConfirm={handleAllSessionsRevoke} + onDismiss={dismissLogoutConfirmationModal} + /> +
+
+
+ ); +}); +UserSessions.displayName = 'UserSessions'; diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index da4c513388a..34459b9581e 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -335,7 +335,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { if (cfg.featureToggles.alertingTriage) { routes.push({ - path: '/alerting/triage', + path: '/alerting/alerts', roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]), component: importAlertingComponent( () => import(/* webpackChunkName: "AlertingTriage" */ 'app/features/alerting/unified/triage/Triage') diff --git a/public/app/features/alerting/unified/api/receiversApi.ts b/public/app/features/alerting/unified/api/receiversApi.ts index 17d32398b04..9d0778d6e8e 100644 --- a/public/app/features/alerting/unified/api/receiversApi.ts +++ b/public/app/features/alerting/unified/api/receiversApi.ts @@ -1,8 +1,10 @@ /** @deprecated To be deleted - use alertingApiServer API instead */ import { ContactPointsState } from 'app/features/alerting/unified/types/alerting'; +import { Receiver, TestReceiversAlert, TestReceiversResult } from 'app/plugins/datasource/alertmanager/types'; import { CONTACT_POINTS_STATE_INTERVAL_MS } from '../utils/constants'; +import { getDatasourceAPIUid } from '../utils/datasource'; import { alertingApi } from './alertingApi'; import { fetchContactPointsState } from './grafana'; @@ -19,9 +21,34 @@ export const receiversApi = alertingApi.injectEndpoints({ } }, }), + testIntegration: build.mutation({ + query: ({ alertManagerSourceName, receivers, alert }) => ({ + method: 'POST', + data: { + receivers, + alert, + }, + url: `/api/alertmanager/${getDatasourceAPIUid(alertManagerSourceName)}/config/api/v1/receivers/test`, + showErrorAlert: false, + showSuccessAlert: false, + }), + transformResponse: (response: TestReceiversResult) => { + // Check if the response contains errors even though the HTTP status was 200 + if (receiversResponseContainsErrors(response)) { + throw new Error(getReceiverResultError(response)); + } + return response; + }, + }), }), }); +interface TestReceiversOptions { + alertManagerSourceName: string; + receivers: Receiver[]; + alert?: TestReceiversAlert; +} + export const useGetContactPointsState = (alertManagerSourceName: string) => { const contactPointsStateEmpty: ContactPointsState = { receivers: {}, errorCount: 0 }; const { currentData: contactPointsState } = receiversApi.useContactPointsStateQuery( @@ -33,3 +60,22 @@ export const useGetContactPointsState = (alertManagerSourceName: string) => { ); return contactPointsState ?? contactPointsStateEmpty; }; + +export const { useTestIntegrationMutation } = receiversApi; + +// Helper functions for checking receiver test results +function receiversResponseContainsErrors(result: TestReceiversResult): boolean { + return result.receivers.some((receiver) => + receiver.grafana_managed_receiver_configs.some((config) => config.status === 'failed') + ); +} + +function getReceiverResultError(receiversResult: TestReceiversResult): string { + return receiversResult.receivers + .flatMap((receiver) => + receiver.grafana_managed_receiver_configs + .filter((config) => config.status === 'failed') + .map((config) => config.error ?? 'Unknown error.') + ) + .join('; '); +} diff --git a/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.tsx b/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.tsx index 4f2c364859a..bf2a253d7e8 100644 --- a/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.tsx +++ b/public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.tsx @@ -385,7 +385,6 @@ function TargetFolderNotEmptyWarning({ targetFolderRules }: { targetFolderRules: )} isOpen={showTargetRules} onToggle={toggleShowTargetRules} - collapsible={true} > diff --git a/public/app/features/alerting/unified/components/import-to-gma/ImportToGMARules.tsx b/public/app/features/alerting/unified/components/import-to-gma/ImportToGMARules.tsx index bfdb5b3303c..d88d446642d 100644 --- a/public/app/features/alerting/unified/components/import-to-gma/ImportToGMARules.tsx +++ b/public/app/features/alerting/unified/components/import-to-gma/ImportToGMARules.tsx @@ -150,7 +150,6 @@ const ImportToGMARules = () => { label={t('alerting.import-to-gma.additional-settings', 'Additional settings')} isOpen={optionsShowing} onToggle={toggleOptions} - collapsible={true} > diff --git a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx index dc87060c03d..61ce391d779 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx @@ -130,7 +130,6 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi /> ({ const selectedType = watch(typeFieldPath) ?? defaultValues.type; const parse_mode = watch(`${settingsFieldPath}.parse_mode`); - const { loading: testingReceiver } = useUnifiedAlertingSelector((state) => state.testReceivers); // TODO I don't like integration specific code here but other ways require a bigger refactoring const onCallIntegrationType = watch(`${settingsFieldPath}.integration_type`); @@ -232,14 +230,7 @@ export function ChannelSubForm({
{isTestable && onTest && isTestAvailable && ( - )} diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index 8971c9b34d7..de17ab6c7c0 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -13,12 +13,10 @@ import { canEditEntity } from 'app/features/alerting/unified/utils/k8s/utils'; import { GrafanaManagedContactPoint, GrafanaManagedReceiverConfig, - TestReceiversAlert, + Receiver, } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types/store'; import { alertmanagerApi } from '../../../api/alertmanagerApi'; -import { testReceiversAction } from '../../../state/actions'; import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form'; import { formChannelValuesToGrafanaChannelConfig, @@ -52,7 +50,6 @@ interface Props { const { useGrafanaNotifiersQuery } = alertmanagerApi; export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }: Props) => { - const dispatch = useDispatch(); const [createContactPoint] = useCreateContactPoint({ alertmanager: GRAFANA_RULES_SOURCE_NAME, }); @@ -71,7 +68,7 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode } const { data: grafanaNotifiers = [], isLoading: isLoadingNotifiers } = useGrafanaNotifiersQuery(); - const [testChannelValues, setTestChannelValues] = useState(); + const [testReceivers, setTestReceivers] = useState(); // transform receiver DTO to form values const [existingValue, id2original] = useMemo((): [ @@ -112,27 +109,17 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode } }; const onTestChannel = (values: GrafanaChannelValues) => { - setTestChannelValues(values); - }; + const existing: GrafanaManagedReceiverConfig | undefined = id2original[values.__id]; + const chan = formChannelValuesToGrafanaChannelConfig(values, defaultChannelValues, 'test', existing); - const testNotification = (alert?: TestReceiversAlert) => { - if (testChannelValues) { - const existing: GrafanaManagedReceiverConfig | undefined = id2original[testChannelValues.__id]; - const chan = formChannelValuesToGrafanaChannelConfig(testChannelValues, defaultChannelValues, 'test', existing); + const receivers: Receiver[] = [ + { + name: 'test', + grafana_managed_receiver_configs: [chan], + }, + ]; - const payload = { - alertManagerSourceName: GRAFANA_RULES_SOURCE_NAME, - receivers: [ - { - name: 'test', - grafana_managed_receiver_configs: [chan], - }, - ], - alert, - }; - - dispatch(testReceiversAction(payload)); - } + setTestReceivers(receivers); }; // If there is no contact point it means we're creating a new one, so scoped permissions doesn't exist yet @@ -192,11 +179,14 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode } editMode && contactPoint && showManageContactPointPermissions(GRAFANA_RULES_SOURCE_NAME, contactPoint) } /> - setTestChannelValues(undefined)} - isOpen={!!testChannelValues} - onTest={(alert) => testNotification(alert)} - /> + {testReceivers && ( + setTestReceivers(undefined)} + isOpen={!!testReceivers} + alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME} + receivers={testReceivers} + /> + )} ); }; diff --git a/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx b/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx index a0f81586269..78cdb95fa89 100644 --- a/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx @@ -4,18 +4,21 @@ import { FormProvider, useForm } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Button, Label, Modal, RadioButtonGroup, useStyles2 } from '@grafana/ui'; -import { TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types'; +import { Alert, Button, Label, Modal, RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import { Receiver, TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types'; import { Annotations, Labels } from 'app/types/unified-alerting-dto'; +import { useTestIntegrationMutation } from '../../../api/receiversApi'; import { defaultAnnotations } from '../../../utils/constants'; +import { stringifyErrorLike } from '../../../utils/misc'; import AnnotationsStep from '../../rule-editor/AnnotationsStep'; import LabelsField from '../../rule-editor/labels/LabelsField'; interface Props { isOpen: boolean; onDismiss: () => void; - onTest: (alert?: TestReceiversAlert) => void; + alertManagerSourceName: string; + receivers: Receiver[]; } type AnnoField = { @@ -40,14 +43,17 @@ const defaultValues: FormFields = { labels: [{ key: '', value: '' }], }; -export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => { +export const TestContactPointModal = ({ isOpen, onDismiss, alertManagerSourceName, receivers }: Props) => { const [notificationType, setNotificationType] = useState(NotificationType.predefined); const styles = useStyles2(getStyles); const formMethods = useForm({ defaultValues, mode: 'onBlur' }); + const [testIntegration, { isLoading, error, isSuccess }] = useTestIntegrationMutation(); + + const onSubmit = async (data: FormFields) => { + let alert: TestReceiversAlert | undefined; - const onSubmit = (data: FormFields) => { if (notificationType === NotificationType.custom) { - const alert = { + alert = { annotations: data.annotations .filter(({ key, value }) => !!key && !!value) .reduce((acc, { key, value }) => { @@ -59,10 +65,13 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => { return { ...acc, [key]: value }; }, {}), }; - onTest(alert); - } else { - onTest(); } + + await testIntegration({ + alertManagerSourceName, + receivers, + alert, + }).unwrap(); }; return ( @@ -71,6 +80,19 @@ export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => { isOpen={isOpen} title={t('alerting.test-contact-point-modal.title-test-contact-point', 'Test contact point')} > + {Boolean(error) && ( + + {stringifyErrorLike(error)} + + )} + + {isSuccess && ( + + )} +